[아이티센 부트캠프] Spring Boot 실습

이언덕·2026년 5월 17일

아이티센 부트캠프

목록 보기
70/115
post-thumbnail

문제

먼저 문제 원본은 아래와 같다.

문제에서 실제로 구현해야 하는 조건만 다시 정리하면 이렇다.

  • TeamDTO 클래스를 구현한다.
  • TeamDTO에는 teamName과 teamMember 멤버변수가 필요하다.
  • teamMember는 TeamMemberVO 객체들을 담는 List 타입으로 만든다.
  • TeamDTO는 Lombok을 사용해서 getter, setter, toString()을 생성한다.
  • TeamMemberVO 클래스를 구현한다.
  • TeamMemberVO에는 name, nickName, food 멤버변수가 필요하다.
  • TeamMemberVO는 Lombok을 사용해서 getter, 전체 생성자, toString()을 생성한다.
  • MyTeamController1 클래스를 구현한다.
  • 컨트롤러 매핑명은 /myTeam1로 한다.
  • 응답 화면은 myTeamView.html에서 처리한다.
  • 컨트롤러 메서드는 1개만 구현한다.
  • TeamDTO 객체는 myteam이라는 이름으로 보관한다.
  • 클라이언트가 팀명 출력을 요청하면 TeamDTO 객체에 팀명을 저장해서 화면에 전달한다.
  • 클라이언트가 팀원 정보 출력을 요청하면 TeamMemberVO 객체를 3~5개 생성하고 ArrayList에 저장한 뒤, 이 리스트를 TeamDTO 객체에 저장해서 화면에 전달한다.




문제 해석

이 문제는 단순히 팀명이나 팀원 이름을 화면에 출력하는 문제가 아니다.
DTO, VO, Controller, View가 서로 어떻게 연결되는지 확인하는 문제다.


먼저 TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.
한 명의 이름, 별명, 좋아하는 음식을 저장한다.


그 다음 TeamDTO는 화면에 전달할 팀 전체 정보를 담는 객체다.
팀명만 담을 수도 있고, 여러 명의 팀원 정보를 리스트로 담을 수도 있다.


여기서 중요한 점은 팀원이 한 명이 아니라 여러 명이라는 점이다.
팀원 한 명은 TeamMemberVO 객체 하나로 만들 수 있지만, 팀원 여러 명은 객체 하나로 표현할 수 없다.
그래서 여러 개의 TeamMemberVO 객체를 List에 담아야 한다.


컨트롤러는 사용자가 어떤 버튼을 눌렀는지 확인한다.
사용자가 우리 팀명 버튼을 누르면 TeamDTO에 팀명만 저장한다.
사용자가 우리 팀원 버튼을 누르면 TeamMemberVO 객체 여러 개를 만들고, 이 객체들을 ArrayList에 담은 뒤 TeamDTO에 저장한다.


마지막으로 myTeamView.html은 컨트롤러가 전달한 myteam 객체를 꺼내 화면에 출력한다.
팀명 데이터가 있으면 팀명을 출력하고, 팀원 리스트가 있으면 반복문으로 팀원 목록을 출력한다.


즉 이 실습의 핵심 흐름은 아래와 같다.


버튼 클릭 → type 요청 파라미터 전달 → Controller에서 type 값 확인 → TeamDTO 생성 → 팀명 또는 팀원 목록 저장 → ModelAndView에 myteam 이름으로 저장 → myTeamView.html에서 조건에 따라 출력




풀이 순서

이번 실습은 객체 구조를 먼저 만들고, 그 객체를 컨트롤러에서 채운 뒤, 화면에서 꺼내 출력하는 순서로 진행했다.
1. TeamMemberVO 클래스 생성
2. TeamMemberVO에 name, nickName, food 멤버변수 선언
3. Lombok으로 getter, 전체 생성자, toString() 생성
4. TeamDTO 클래스 생성
5. TeamDTO에 teamName, teamMember 멤버변수 선언
6. teamMember를 List<TeamMemberVO> 타입으로 작성
7. Lombok으로 getter, setter, toString() 생성
8. DTO와 VO의 역할 차이 정리
9. myTeamView.html에서 요청 버튼 만들기
10. 버튼의 name과 value로 요청 구분값 전달하기
11. MyTeamController1에서 /myTeam1 요청 처리하기
12. @RequestParam으로 type 요청 파라미터 받기
13. if문과 equals()로 팀명 요청과 팀원 요청 구분하기
14. 팀명 요청이면 TeamDTO에 팀명 저장하기
15. 팀원 요청이면 TeamMemberVO 객체 여러 개 생성하기
16. 생성한 팀원 객체들을 ArrayList에 저장하기
17. 팀원 리스트를 TeamDTO에 저장하기
18. ModelAndView에 myteam이라는 이름으로 TeamDTO 저장하기
19. myTeamView.html에서 팀명 또는 팀원 목록 출력하기




DTO와 VO의 차이

이번 문제에서는 TeamDTO와 TeamMemberVO가 같이 나온다.
이름이 비슷해서 처음에는 둘 다 데이터를 담는 객체처럼 보일 수 있다.


하지만 이 실습에서는 두 객체의 역할을 나누어 이해하는 것이 좋다.
TeamDTO는 화면으로 전달할 전체 데이터를 담는 객체이고, TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.


먼저 DTO는 Data Transfer Object의 줄임말이다.
이름 그대로 데이터를 전달하기 위한 객체다.
즉 컨트롤러에서 만든 데이터를 화면으로 넘기거나, 화면에서 받은 데이터를 컨트롤러로 전달할 때 사용하는 객체라고 보면 된다.


이번 실습에서 TeamDTO는 화면으로 전달할 팀 전체 정보를 담는다.
팀명만 요청한 경우에는 teamName만 담고, 팀원 정보를 요청한 경우에는 teamMember 리스트를 담는다.
그래서 TeamDTO는 요청 결과에 따라 담는 데이터가 달라질 수 있다.


반면 VO는 Value Object의 줄임말이다.
일반적으로는 하나의 값을 표현하는 객체, 또는 특정 정보 묶음을 값처럼 다루는 객체를 의미한다.


이번 실습에서 TeamMemberVO는 팀원 한 명의 정보를 담는다.
팀원 한 명은 이름, 별명, 좋아하는 음식으로 표현된다.
그래서 TeamMemberVO 객체 하나는 팀원 한 명의 정보 묶음이라고 볼 수 있다.


즉 이번 실습 기준으로 정리하면 아래와 같다.

  • TeamDTO는 화면에 전달할 전체 결과 데이터를 담는 객체다.
  • TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.
  • TeamDTO 안에는 팀원 여러 명을 담기 위해 List<TeamMemberVO>가 들어간다.

이 관계를 구조로 생각하면 아래와 같다.


TeamDTO
└── teamName
└── teamMember = List<TeamMemberVO>
├── TeamMemberVO 1명
├── TeamMemberVO 1명
├── TeamMemberVO 1명
└── TeamMemberVO 1명


그래서 TeamDTO와 TeamMemberVO는 서로 따로 노는 객체가 아니다.
TeamDTO가 팀 전체 데이터를 화면으로 전달하고, 그 안에 들어 있는 TeamMemberVO들이 각각 팀원 한 명씩을 표현하는 구조다.


정리하면 DTO는 주로 데이터를 전달하는 큰 그릇이고, VO는 그 안에 들어가는 하나의 값 묶음이라고 이해하면 된다.
이번 실습에서는 TeamDTO가 응답 전체를 담고, TeamMemberVO가 팀원 한 명의 정보를 담는다.




자세한 풀이

이제 풀이 순서에 맞춰 실제 구현 흐름을 정리한다.
이번 실습의 핵심은 객체를 따로따로 만드는 것이 아니라, 객체들이 어떤 관계로 연결되는지 이해하는 것이다.

1. TeamMemberVO 클래스 만들기

TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.
팀원 한 명에게 필요한 정보는 이름, 별명, 좋아하는 음식이다.


그래서 멤버변수는 name, nickName, food로 구성했다.
이 객체는 값을 만든 뒤 수정하는 용도보다, 팀원 한 명의 정보를 한 번에 담아두는 용도에 가깝다.


그래서 setter는 사용하지 않고, 전체 값을 받는 생성자로 값을 저장한다.
그리고 화면에서 값을 꺼낼 수 있도록 getter를 사용한다.

package com.example.springlab.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public class TeamMemberVO {
    private String name;
    private String nickName;
    private String food;
}

@AllArgsConstructor는 모든 멤버변수를 매개변수로 받는 생성자를 자동으로 만들어준다.
따라서 아래처럼 팀원 객체를 만들 수 있다.

new TeamMemberVO("이현석", "이언덕", "김치찌개")

이 코드를 실행하면 name에는 이현석, nickName에는 이언덕, food에는 김치찌개가 저장된다.


데이터 흐름

TeamMemberVO 객체 하나는 팀원 한 명을 의미한다.
예를 들어 아래 코드는 이현석이라는 팀원 한 명의 정보를 객체 하나로 만든 것이다.

TeamMemberVO member = new TeamMemberVO("이현석", "이언덕", "김치찌개");

이 객체 안에는 세 개의 값이 들어 있다.


name → 이현석
nickName → 이언덕
food → 김치찌개


나중에 Thymeleaf 화면에서 ${member.name}, ${member.nickName}, ${member.food}라고 작성하면 내부적으로 getter를 통해 이 값들을 꺼내 출력한다.


2. TeamDTO 클래스 만들기

TeamDTO는 화면으로 전달할 팀 전체 정보를 담는 객체다.
팀원 한 명을 담는 TeamMemberVO와 역할이 다르다.


TeamDTO에는 팀명인 teamName과 팀원 목록인 teamMember가 들어간다.
여기서 teamMember는 팀원 한 명이 아니라 여러 명의 팀원 정보를 담아야 하므로 List<TeamMemberVO> 타입으로 작성한다.

package com.example.springlab.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class TeamDTO {
    private String teamName;
    private List<TeamMemberVO> teamMember;
}

여기서 private List<TeamMemberVO> teamMember;는 TeamMemberVO 객체 여러 개를 담을 수 있는 팀원 목록 변수를 선언한 것이다.
TeamMemberVO 하나가 팀원 한 명이라면, List<TeamMemberVO>는 팀원 여러 명을 담는 목록이다.


즉 teamMember에는 팀원 이름만 따로따로 들어가는 것이 아니라, 이름, 별명, 좋아하는 음식을 가진 TeamMemberVO 객체들이 들어간다.
그래서 화면에서는 이 리스트를 반복하면서 팀원 한 명씩 꺼내 출력할 수 있다.


TeamDTO에는 setter가 필요하다.
이유는 컨트롤러에서 빈 TeamDTO 객체를 먼저 만들고, 요청 종류에 따라 팀명만 넣거나 팀원 리스트를 넣기 때문이다.


예를 들어 팀명 요청이면 아래처럼 팀명만 저장한다.

team.setTeamName("우주수석");

팀원 요청이면 아래처럼 팀원 리스트를 저장한다.

team.setTeamMember(members);

즉 TeamDTO는 요청 결과에 따라 값이 나중에 채워지는 객체이기 때문에 setter를 사용한다.


왜 List를 사용하는가

팀원 정보는 한 명만 있는 것이 아니다.
이번 실습에서는 팀원 4명의 정보를 화면에 출력해야 한다.


팀원 한 명은 TeamMemberVO 객체 하나로 표현할 수 있다.
하지만 팀원 4명은 TeamMemberVO 객체 4개가 필요하다.


이 여러 개의 객체를 하나로 묶어서 관리하기 위해 List를 사용한다.

List<TeamMemberVO> members = new ArrayList<>();
members.add(new TeamMemberVO("이현석", "이언덕", "김치찌개"));
members.add(new TeamMemberVO("김윤우", "유누", "마라탕"));
members.add(new TeamMemberVO("강민주", "만두", "샤브샤브"));
members.add(new TeamMemberVO("김민수", "라멘집아들", "라멘"));

List<TeamMemberVO>는 TeamMemberVO 객체만 담을 수 있는 목록이다.
즉 문자열을 그냥 여러 개 담는 것이 아니라, 팀원 한 명의 정보가 들어 있는 객체들을 순서대로 담는다.


이렇게 만든 리스트를 TeamDTO에 저장하면 화면에서는 myteam.teamMember를 통해 팀원 목록 전체를 사용할 수 있다.


정리하면 아래와 같다.


TeamMemberVO 1개 = 팀원 1명
List<TeamMemberVO> = 팀원 여러 명
TeamDTO = 팀명과 팀원 목록을 화면으로 전달하는 객체


3. myTeamView.html에서 요청 버튼 만들기

화면에는 우리 팀명 버튼과 우리 팀원 버튼이 필요하다.
두 버튼은 같은 컨트롤러인 /myTeam1로 요청을 보내지만, 어떤 버튼을 눌렀는지는 구분되어야 한다.


그래서 버튼의 name은 둘 다 type으로 맞추고, value만 다르게 작성했다.

<form method="get" action="/myTeam1">
    <button type="submit" name="type" value="teamName">우리 팀명</button>
    <button type="submit" name="type" value="teamMember">우리 팀원</button>
</form>

우리 팀명 버튼을 누르면 요청 주소는 아래처럼 만들어진다.

/myTeam1?type=teamName

우리 팀원 버튼을 누르면 요청 주소는 아래처럼 만들어진다.

/myTeam1?type=teamMember

즉 컨트롤러는 type 값을 보고 사용자가 어떤 버튼을 눌렀는지 알 수 있다.


데이터 흐름

처음 화면에서는 버튼만 보인다.
사용자가 우리 팀명 버튼을 누르면 type=teamName 값이 서버로 전달된다.
사용자가 우리 팀원 버튼을 누르면 type=teamMember 값이 서버로 전달된다.


이 값은 컨트롤러에서 @RequestParam으로 받는다.


버튼 클릭 → type 값 생성 → /myTeam1 요청 → Controller에서 type 값 받기


4. MyTeamController1에서 요청 처리하기

컨트롤러는 /myTeam1 요청을 처리한다.
요청 파라미터 type 값을 받아서 팀명 요청인지 팀원 요청인지 구분한다.


이때 required = false를 사용했다.
처음 /myTeam1에 접속할 때는 아직 버튼을 누르지 않았기 때문에 type 값이 없을 수 있다.
required = false를 사용하면 type 값이 없어도 오류가 나지 않고 처음 화면을 보여줄 수 있다.

@GetMapping("/myTeam1")
public ModelAndView myTeam(@RequestParam(value = "type", required = false) String type) {

컨트롤러 전체 코드는 아래와 같다.

package com.example.springlab.controller;
import com.example.springlab.domain.TeamDTO;
import com.example.springlab.domain.TeamMemberVO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import java.util.ArrayList;
import java.util.List;
@Controller
public class MyTeamController1 {
    @GetMapping("/myTeam1")
    public ModelAndView myTeam(@RequestParam(value = "type", required = false) String type) {
        ModelAndView mav = new ModelAndView();
        TeamDTO team = new TeamDTO();
        if ("teamName".equals(type)) {
            team.setTeamName("우주수석");
        } else if ("teamMember".equals(type)) {
            team.setTeamName("우주수석");
            List<TeamMemberVO> members = new ArrayList<>();
            members.add(new TeamMemberVO("이현석", "이언덕", "김치찌개"));
            members.add(new TeamMemberVO("김윤우", "유누", "마라탕"));
            members.add(new TeamMemberVO("강민주", "만두", "샤브샤브"));
            members.add(new TeamMemberVO("김민수", "라멘집아들", "라멘"));
            team.setTeamMember(members);
        }
        mav.addObject("myteam", team);
        mav.setViewName("myTeamView");
        return mav;
    }
}

처음에는 ModelAndView 객체를 만든다.
ModelAndView는 화면에 전달할 데이터와 이동할 뷰 이름을 함께 담을 수 있는 객체다.


그 다음 TeamDTO 객체를 만든다.
이 객체는 최종적으로 화면에 전달할 팀 정보를 담는 역할을 한다.


여기서 팀원 요청일 때 중요한 코드가 아래 한 줄이다.

List<TeamMemberVO> members = new ArrayList<>();

이 코드는 팀원 여러 명을 담을 빈 목록을 준비하는 코드다.


앞에서 TeamDTO에는 private List<TeamMemberVO> teamMember;가 있었다.
이 코드는 TeamDTO 안에 팀원 목록을 저장할 수 있는 멤버변수를 선언한 것이다.
하지만 멤버변수 선언만으로는 컨트롤러 안에서 팀원 객체들을 바로 추가할 수 있는 실제 리스트가 만들어지는 것은 아니다.


그래서 컨트롤러에서는 팀원 객체들을 먼저 담아둘 실제 리스트 객체를 따로 만들어야 한다.
그 역할을 하는 코드가 List<TeamMemberVO> members = new ArrayList<>();다.


왼쪽의 List<TeamMemberVO> members는 변수 선언이다.
members라는 변수는 TeamMemberVO 객체만 여러 개 담는 목록으로 사용하겠다는 의미다.


오른쪽의 new ArrayList<>()는 실제 저장 공간을 만드는 코드다.
ArrayList는 데이터를 순서대로 저장할 수 있는 리스트 객체다.
따라서 이 코드가 실행되어야 members.add(...)로 팀원 객체들을 하나씩 넣을 수 있다.


즉 이 코드는 아래처럼 나누어 이해하면 된다.

  • List<TeamMemberVO>는 팀원 객체만 담는 목록 타입이다.
  • members는 컨트롤러 안에서 사용할 팀원 목록 변수 이름이다.
  • new ArrayList<>()는 실제로 팀원 객체들을 담을 빈 리스트를 만드는 코드다.

그 다음 아래 코드처럼 add()를 사용해서 팀원 객체를 하나씩 넣는다.

members.add(new TeamMemberVO("이현석", "이언덕", "김치찌개"));
members.add(new TeamMemberVO("김윤우", "유누", "마라탕"));
members.add(new TeamMemberVO("강민주", "만두", "샤브샤브"));
members.add(new TeamMemberVO("김민수", "라멘집아들", "라멘"));

첫 번째 add()에서는 이현석 팀원 객체가 들어간다.
두 번째 add()에서는 김윤우 팀원 객체가 들어간다.
세 번째 add()에서는 강민주 팀원 객체가 들어간다.
네 번째 add()에서는 김민수 팀원 객체가 들어간다.


이 과정이 끝나면 members 리스트 안에는 TeamMemberVO 객체 4개가 들어 있다.
이 리스트를 아래 코드로 TeamDTO에 저장한다.

team.setTeamMember(members);

즉 members는 컨트롤러 안에서 팀원 객체들을 임시로 모아두는 리스트다.
그리고 team.setTeamMember(members)를 실행해야 그 리스트가 TeamDTO 안의 teamMember에 저장된다.


마지막으로 mav.addObject("myteam", team)은 team 객체를 myteam이라는 이름으로 화면에 전달한다.
mav.setViewName("myTeamView")는 응답 화면으로 myTeamView.html을 사용하겠다는 의미다.


if문에서 equals를 사용한 이유

type은 문자열이다.
문자열 값을 비교할 때는 ==보다 equals()를 사용해야 한다.


==는 문자열의 내용이 같은지 비교하는 것이 아니라, 두 객체가 같은 객체인지 비교하는 쪽에 가깝다.
반면 equals()는 문자열 안에 들어 있는 실제 글자 값이 같은지 비교한다.


그래서 type 값이 teamName인지 확인할 때는 아래처럼 작성한다.

if ("teamName".equals(type)) {
    team.setTeamName("우주수석");
}

여기서 "teamName".equals(type)처럼 문자열 리터럴을 앞에 둔 이유도 중요하다.
처음 /myTeam1에 접속하면 type 값이 없어서 null일 수 있다.


만약 아래처럼 작성하면,

if (type.equals("teamName")) {
    team.setTeamName("우주수석");
}

type이 null일 때 NullPointerException이 발생할 수 있다.


하지만 "teamName".equals(type)처럼 작성하면 type이 null이어도 오류가 발생하지 않고, 그냥 false가 된다.
그래서 요청 파라미터가 없을 수도 있는 상황에서는 이 방식이 더 안전하다.


정리하면 아래와 같다.

  • 문자열 내용 비교는 equals()를 사용한다.
  • type은 처음 접속 시 null일 수 있다.
  • "teamName".equals(type)처럼 상수 문자열을 앞에 두면 NullPointerException을 피할 수 있다.



5. 팀명 요청과 팀원 요청의 데이터 흐름

우리 팀명 버튼을 누르면 /myTeam1?type=teamName 요청이 들어온다.
그러면 type 값은 teamName이 된다.


이때 아래 조건이 참이 된다.

if ("teamName".equals(type)) {
    team.setTeamName("우주수석");
}

그래서 TeamDTO의 teamName에 우주수석이 저장된다.
이 요청에서는 팀원 목록이 필요 없기 때문에 teamMember는 저장하지 않는다.


반대로 우리 팀원 버튼을 누르면 /myTeam1?type=teamMember 요청이 들어온다.
그러면 아래 조건이 참이 된다.

else if ("teamMember".equals(type)) {
    team.setTeamName("우주수석");
    List<TeamMemberVO> members = new ArrayList<>();
    members.add(new TeamMemberVO("이현석", "이언덕", "김치찌개"));
    members.add(new TeamMemberVO("김윤우", "유누", "마라탕"));
    members.add(new TeamMemberVO("강민주", "만두", "샤브샤브"));
    members.add(new TeamMemberVO("김민수", "라멘집아들", "라멘"));
    team.setTeamMember(members);
}

이 경우에는 팀원 소개 제목에 팀명도 같이 사용해야 하므로 teamName도 저장한다.
그리고 팀원 4명의 정보를 TeamMemberVO 객체로 만든 뒤 members 리스트에 추가한다.
마지막으로 team.setTeamMember(members)를 실행해서 팀원 리스트를 TeamDTO에 저장한다.


즉 흐름은 아래와 같다.


우리 팀명 클릭 → type=teamName → teamName 저장 → myteam으로 전달 → 팀명 문장 출력


우리 팀원 클릭 → type=teamMember → teamName 저장 → TeamMemberVO 4개 생성 → members 리스트에 저장 → TeamDTO의 teamMember에 저장 → myteam으로 전달 → 팀원 목록 출력


6. myTeamView.html에서 결과 출력하기

myTeamView.html에서는 컨트롤러가 전달한 myteam 객체를 사용한다.
팀명 요청일 때는 팀명 문장만 출력하고, 팀원 요청일 때는 팀원 목록을 출력해야 한다.


전체 코드는 아래와 같다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>우리팀 소개</h1>
<hr/>
<form method="get" action="/myTeam1">
    <button type="submit" name="type" value="teamName">우리 팀명</button>
    <button type="submit" name="type" value="teamMember">우리 팀원</button>
</form>
<h2 th:if="${myteam.teamName != null and myteam.teamMember == null}">
    우리 팀의 팀명은 <span th:text="${myteam.teamName}">4조</span>입니다.
</h2>
<div th:if="${myteam.teamMember != null}">
    <h2>
        <span th:text="${myteam.teamName}">4조</span>팀의 팀원들을 소개합니다.
    </h2>
    <ul>
        <li th:each="member : ${myteam.teamMember}">
            <span th:text="${member.name}"></span>
            <span th:text="${member.nickName}"></span>
            <span th:text="${member.food}"></span>
        </li>
    </ul>
</div>
</body>
</html>

팀명 문장은 아래 조건을 만족할 때만 출력된다.

<h2 th:if="${myteam.teamName != null and myteam.teamMember == null}">

이 조건은 teamName은 있고, teamMember는 없을 때만 참이다.
즉 우리 팀명 버튼을 눌렀을 때만 팀명 문장이 출력된다.


팀원 목록은 아래 조건을 만족할 때만 출력된다.

<div th:if="${myteam.teamMember != null}">

이 조건은 팀원 리스트가 있을 때만 참이다.
즉 우리 팀원 버튼을 눌렀을 때만 팀원 목록 영역이 출력된다.


팀원 목록 출력에는 th:each를 사용했다.

<li th:each="member : ${myteam.teamMember}">
    <span th:text="${member.name}"></span>
    <span th:text="${member.nickName}"></span>
    <span th:text="${member.food}"></span>
</li>

myteam.teamMember는 List<TeamMemberVO>다.
따라서 리스트 안에 있는 TeamMemberVO 객체를 하나씩 꺼내야 한다.


th:each="member : ${myteam.teamMember}"는 리스트에서 팀원 객체를 하나씩 꺼내 member라는 이름으로 사용하겠다는 의미다.
그래서 반복문 안에서는 ${member.name}, ${member.nickName}, ${member.food}로 각 팀원의 정보를 출력할 수 있다.


7. 트러블 슈팅: 팀원 버튼을 눌렀을 때 팀명 문장까지 같이 출력되는 문제

처음에는 팀명 출력 조건을 아래처럼 작성했다.

<h2 th:if="${myteam.teamName != null}">
    우리 팀의 팀명은 <span th:text="${myteam.teamName}">4조</span>입니다.
</h2>

이 조건은 teamName 값만 있으면 무조건 참이 된다.


그런데 팀원 목록 화면에서도 제목에 팀명을 사용해야 해서 컨트롤러에서 teamName을 같이 저장했다.
그 결과 우리 팀원 버튼을 눌렀을 때도 teamName이 존재하게 되었고, 팀명 문장까지 같이 출력되는 문제가 생겼다.


처음 의도한 결과는 아래와 같았다.

  • 우리 팀명 버튼 클릭 시 팀명 문장만 출력
  • 우리 팀원 버튼 클릭 시 팀원 목록만 출력

그래서 팀명 문장 조건을 더 정확하게 바꿨다.

<h2 th:if="${myteam.teamName != null and myteam.teamMember == null}">

이 조건은 teamName은 있지만 teamMember는 없을 때만 참이다.
즉 팀명만 요청한 경우에만 팀명 문장이 출력된다.


팀원 요청에서는 teamName도 있고 teamMember도 있다.
따라서 myteam.teamMember == null 조건이 거짓이 되기 때문에 팀명 문장은 출력되지 않는다.
대신 팀원 목록 영역만 출력된다.


이 문제를 통해 th:if는 단순히 값이 있는지만 보는 것이 아니라, 화면에서 어떤 상황을 구분해야 하는지에 맞춰 조건을 정확하게 작성해야 한다는 점을 확인했다.




최종 코드

최종적으로 필요한 파일은 TeamMemberVO.java, TeamDTO.java, MyTeamController1.java, myTeamView.html이다.
앞에서 단계별로 나누어 확인한 코드를 실제 실행 가능한 흐름으로 다시 정리하면 아래와 같다.

TeamMemberVO.java

package com.example.springlab.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@AllArgsConstructor
public class TeamMemberVO {
    private String name;
    private String nickName;
    private String food;
}

TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.
@AllArgsConstructor가 생성자를 만들어주기 때문에 팀원 정보를 한 번에 넣어 객체를 생성할 수 있다.


TeamDTO.java

package com.example.springlab.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class TeamDTO {
    private String teamName;
    private List<TeamMemberVO> teamMember;
}

TeamDTO는 화면으로 전달할 팀 전체 정보를 담는 객체다.
teamName에는 팀명이 들어가고, teamMember에는 여러 명의 팀원 정보를 담은 리스트가 들어간다.


MyTeamController1.java

package com.example.springlab.controller;
import com.example.springlab.domain.TeamDTO;
import com.example.springlab.domain.TeamMemberVO;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import java.util.ArrayList;
import java.util.List;
@Controller
public class MyTeamController1 {
    @GetMapping("/myTeam1")
    public ModelAndView myTeam(@RequestParam(value = "type", required = false) String type) {
        ModelAndView mav = new ModelAndView();
        TeamDTO team = new TeamDTO();
        if ("teamName".equals(type)) {
            team.setTeamName("우주수석");
        } else if ("teamMember".equals(type)) {
            team.setTeamName("우주수석");
            List<TeamMemberVO> members = new ArrayList<>();
            members.add(new TeamMemberVO("이현석", "이언덕", "김치찌개"));
            members.add(new TeamMemberVO("김윤우", "유누", "마라탕"));
            members.add(new TeamMemberVO("강민주", "만두", "샤브샤브"));
            members.add(new TeamMemberVO("김민수", "라멘집아들", "라멘"));
            team.setTeamMember(members);
        }
        mav.addObject("myteam", team);
        mav.setViewName("myTeamView");
        return mav;
    }
}

컨트롤러는 type 값에 따라 팀명 요청과 팀원 요청을 구분한다.
구분한 결과를 TeamDTO에 저장하고, myteam이라는 이름으로 화면에 전달한다.


myTeamView.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>우리팀 소개</h1>
<hr/>
<form method="get" action="/myTeam1">
    <button type="submit" name="type" value="teamName">우리 팀명</button>
    <button type="submit" name="type" value="teamMember">우리 팀원</button>
</form>
<h2 th:if="${myteam.teamName != null and myteam.teamMember == null}">
    우리 팀의 팀명은 <span th:text="${myteam.teamName}">4조</span>입니다.
</h2>
<div th:if="${myteam.teamMember != null}">
    <h2>
        <span th:text="${myteam.teamName}">4조</span>팀의 팀원들을 소개합니다.
    </h2>
    <ul>
        <li th:each="member : ${myteam.teamMember}">
            <span th:text="${member.name}"></span>
            <span th:text="${member.nickName}"></span>
            <span th:text="${member.food}"></span>
        </li>
    </ul>
</div>
</body>
</html>

myTeamView.html은 버튼을 통해 요청을 보내고, 컨트롤러에서 전달한 myteam 객체를 조건에 따라 출력한다.
팀명만 있을 때는 팀명 문장을 출력하고, 팀원 리스트가 있을 때는 반복문으로 팀원 목록을 출력한다.




실행 결과 확인

처음 /myTeam1에 접속하면 아직 type 값이 없기 때문에 버튼만 보인다.
이때 TeamDTO 객체는 만들어지지만, teamName과 teamMember에는 값이 들어가지 않는다.


우리 팀명 버튼을 클릭하면 /myTeam1?type=teamName 요청이 전송된다.
컨트롤러는 type 값이 teamName인 것을 확인하고, TeamDTO에 팀명 우주수석을 저장한다.
그 결과 화면에는 우리 팀의 팀명은 우주수석입니다.가 출력된다.


우리 팀원 버튼을 클릭하면 /myTeam1?type=teamMember 요청이 전송된다.
컨트롤러는 팀명과 팀원 리스트를 TeamDTO에 저장한다.
그 결과 화면에는 우주수석팀의 팀원들을 소개합니다. 문장과 함께 팀원 목록이 출력된다.


실행 결과 흐름은 아래처럼 정리할 수 있다.

  • 처음 접속: 버튼만 출력
  • 우리 팀명 클릭: 팀명 문장 출력
  • 우리 팀원 클릭: 팀원 소개 제목과 팀원 목록 출력

이 실행 결과를 통해 요청 파라미터 값에 따라 서로 다른 데이터를 만들어 화면에 전달할 수 있다는 점을 확인했다.




실습하면서 확인한 점

이번 실습에서는 Spring MVC에서 요청 파라미터를 기준으로 다른 데이터를 만들고, 그 결과를 객체에 담아 화면으로 전달하는 흐름을 확인했다.
특히 여러 명의 팀원 정보를 다룰 때 왜 List가 필요한지, 문자열 비교에서 왜 equals()를 사용해야 하는지 정리할 수 있었다.

  • DTO는 데이터를 전달하기 위한 객체다.
  • VO는 특정 값 묶음을 표현하는 객체다.
  • 이번 실습에서 TeamDTO는 팀 전체 응답 데이터를 화면으로 전달하는 객체다.
  • 이번 실습에서 TeamMemberVO는 팀원 한 명의 정보를 담는 객체다.
  • 팀원 한 명의 정보는 TeamMemberVO 객체 하나로 표현할 수 있다.
  • 팀원 여러 명의 정보는 TeamMemberVO 객체 여러 개가 필요하다.
  • 여러 객체를 하나로 묶어 관리하려면 List를 사용할 수 있다.
  • List<TeamMemberVO>는 TeamMemberVO 객체만 담는 팀원 목록이다.
  • ArrayList는 실제로 데이터를 순서대로 저장하는 List 구현체다.
  • List<TeamMemberVO> members = new ArrayList<>();는 컨트롤러에서 팀원 객체들을 담을 빈 목록을 준비하는 코드다.
  • private List<TeamMemberVO> teamMember;는 TeamDTO 안에 팀원 목록을 저장할 멤버변수를 선언한 것이다.
  • team.setTeamMember(members)를 해야 컨트롤러에서 만든 팀원 목록이 TeamDTO 안에 저장된다.
  • button의 name과 value를 사용하면 어떤 버튼을 눌렀는지 요청 파라미터로 전달할 수 있다.
  • @RequestParam(value = "type", required = false)를 사용하면 요청 파라미터가 없어도 처음 화면을 보여줄 수 있다.
  • 문자열 비교에서는 ==가 아니라 equals()를 사용한다.
  • "teamName".equals(type)처럼 작성하면 type이 null이어도 오류가 나지 않는다.
  • ModelAndView는 화면에 전달할 데이터와 이동할 화면 이름을 함께 담을 수 있다.
  • mav.addObject("myteam", team)을 사용하면 화면에서 myteam이라는 이름으로 객체를 사용할 수 있다.
  • th:if를 사용하면 조건에 따라 특정 태그를 출력하거나 숨길 수 있다.
  • th:each를 사용하면 리스트에 들어 있는 객체를 하나씩 꺼내 반복 출력할 수 있다.

이번 실습의 핵심은 팀원 정보를 단순 문자열로 여러 개 흩어놓는 것이 아니라, TeamMemberVO 객체로 만들고, 그 객체들을 List에 담은 뒤, TeamDTO를 통해 화면으로 전달하는 흐름이다.
이 흐름을 이해하면 컨트롤러에서 만든 여러 개의 데이터를 화면에서 반복 출력하는 구조를 더 쉽게 이해할 수 있다.

0개의 댓글