본문 바로가기

Back-End 공부/Spring

Controller, Service, Repository에 Dto 객체 추가해 실습하기

 

자바 어노테이션

 

 

어노테이션은 자바 코드에 추가되는 특별한 형태의 메타데이터로, 클래스, 메서드, 변수 등에 대한 추가적인 정보를 제공

어노테이션이 가능한 기술적 근거는 java의 리플렉션, 프록시 등의 기술

  • 리플렉션은 런타임에 클래스의 메타데이터를 조회하거나 "수정"할 수 있게 해주는 Java API ⭐
  • ex) DB 조회기술 - 사용자가 어떤 엔티티를 조회할지 모르기 때문에 이는 런타임에서 결정된다. 이를 객체에 반영해줘야 화면에 객체의 데이터를 띄워줄 수 있다.
  • 스프링의 많은 기능이 런타임에서 동작하는 이유는 소스코드의 유연성과 빌드의 효율성을 위해 사용

 

통신 기능 사전 구현 + 내 소스 코드 => 새 소스 코드 => 컴파일해야지 실행

통신 기능 사전 구현, 내 소스 코드  => 새 소스 코드 X

리플렉션 : 미리 구현되어 있는 어노테이션 코드들을 런타임 상황에서 라이브하게 서로 호환이 되도록 만들어 구동한다.

소스 코드가 가벼워진다는 장점이 있다.

 

 

 

 

   DTO를 사용해보자 ⭐   

 

 

Member는 Getter만 남기고 사용자에게 값을 입력받을 임시 객체 MemberRequestDto를 만들어준다.

Member : DB에 저장될 실제 데이터(엔티티)

MemberRequestDto : 사용자에게 필요한 값만 주거나, 받는 객체

 

 

(1) Member는 DB 엔티티에 저장되어야 할 진짜 데이터이기 때문에 Setter를 제거해 변경 가능성 없애준다

(2) 사용자에게 받는 정보와 실제 저장되어야 할 정보가 다르기 때문에 DTO가 필요하다.

--> 사용자는 id 값과, create_time을 모른다. id와 create_time은 개발자 및 시스템 상에서 필요한 속성이다.

@Getter
public class Member {
    private int id;
    private String name;
    private String email;
    private String password;
    private LocalDateTime create_time;
}

 

@Data
public class MemberRequestDto {
    private String name;
    private String email;
    private String password;
}

 

 

사용자에게 객체를 보여줄 때는 실제 저장된 정보( id, create_time )는 제외하고 보여주어야 한다.

하지만, id 값으로 마지막 실습 예제에서 사용자에게 create_time을 보여주기 때문에 두 속성을 미리 추가해줬다.
사용자에게 반환할 임시 객체 MemberResponseDto를 만들어 생략된 정보로 반환해준다.

@Data
public class MemberResponseDto {
    private int id;
    private String name;
    private String email;
    private String password;
    private LocalDateTime create_time;
}

 

 

 

Controller는 DTO만 사용하고,

Repositoty에서는 Member 객체만 사용한다.

Service에서는 DTO와 Member를 서로 변환해주는 기능을 수행한다.

 

 

MemberRequestDto와 MemberResponseDto를 사용한 Controller, Service, Repository 코드를 살펴보자.

[클래스 다이어그램 추가]

컨트롤러(service 객체 생성) -> 서비스(repository 객체 생성) -> 리포지토리(db 만들어 저장)

 

MemberRequestDto <--> Member

Member <--> MemberResponseDto 변환 과정은 Service가 처리하고, Controller는 Dto객체만 사용함에 유의하자.

 

 

[ MemberController.java ]

✔️ 컨트롤러

사용자 요청 처리, 응답 -> DTO를 Member(DB에 들어갈 진짜 데이터)로 가공하는 과정은 Service에게 넘긴다.

@Controller
@RequestMapping("members")
public class MemberController {
    private final MemberService memberService;
    MemberController(){
        memberService = new MemberService();
    }

    //회원 목록 조회
    @GetMapping()
    public String members(Model model){
        // memberResponseDtos 가져옴
        // 사용자는 Member 객체가 아닌 DTO 객체에 있는 정보만 필요함
        model.addAttribute("members", memberService.members());
        return "member/memberList";
    }

    //회원가입
    @GetMapping("create")
    public String createMember(){
        return "member/member-create-screen";
    }

    @PostMapping("create")
    @ResponseBody
    public String create(MemberRequestDto memberRequestDto){
        System.out.println(memberRequestDto);
        memberService.memberCreate(memberRequestDto);
        return "ok";
    }
}

 

 

[ MemoryMemberRepository.java ]

✔️ 리포지토리

데이터가 담긴 공간.

DB 연결하게 되면 DB에서 조회해서 값을 가져와 담아놓는다.

public class MemoryMemberRepository {
    private final List<Member> memberDB;
    public MemoryMemberRepository(){
        memberDB = new ArrayList<>();
    }

    public List<Member> members(){
        return memberDB;
    }

    public void memberCreate(Member member){
        memberDB.add(member);
    }
}

 

 

[ MemberService.java ]

✔️ 서비스

핵심 로직들을 처리한다.

Controllter와 Repository 사이에서 DTO <--> Member 가공해서 전달한다.

예외 처리한다.

public class MemberService {
    private final MemoryMemberRepository memoryMemberRepository;
    static int total_id;
    public MemberService(){
        memoryMemberRepository = new MemoryMemberRepository();
    }
    public List<MemberResponseDto> members(){
        List<Member> members = memoryMemberRepository.members();
        List<MemberResponseDto> memberResponseDtos = new ArrayList<>();
        for(Member member : members){
            MemberResponseDto memberResponseDto = new MemberResponseDto(member.getId(), member.getName(), member.getEmail(), member.getPassword());
            memberResponseDtos.add(memberResponseDto);
        }
        return memberResponseDtos;
    }

    // 사용자의 입력 값이 담긴 DTO를 통해, 실제 시스템에서 사용되는 정보를 조합해 Member 객체로 변환 후 저장
    public void memberCreate(MemberRequestDto memberRequestDto){
        LocalDateTime now = LocalDateTime.now();
        ++total_id;
        Member member = new Member(total_id, memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword(), now);
        memoryMemberRepository.memberCreate(member);
    }
}

 

 

 

[Home 추가 후 코드]

회원등록 혹은 회원목록조회를 누르면 새 창으로 열렸는데,

두 가지 동작 버튼은 상단에 고정되고, 눌렀을 때 아래 내용만 바뀌도록 수정해보자

 

 

기존 Home.html 코드를 header로 이름을 바꾸고, 이 header 파일을 다른 html에서 헤더로 갖게 수정하면 된다.

<div th:replace="member/header :: headerFragment"></div>

 

 

[ header.html ]

<!DOCTYPE html>
<!-- 템플릿 엔진(JSP, Thymlear)을 사용-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
<div th:fragment="headerFragment">
    <a href="/members/create">회원등록</a>
    <a href="/members">회원목록조회</a>
</div>
</body>
</html>

 

[ member-create-screen.html ]

<!DOCTYPE html>
<!-- 템플릿 엔진(JSP, Thymlear)을 사용-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <div th:replace="member/header :: headerFragment"></div>
    <title>회원가입</title>
</head>
<body>
<form action="http://localhost:8080/members/create" method="POST">
    <p>
        <label>이름</label>
        <input type="text" name="name" maxlength="10" size="15">
    </p>

    <p>
        <label>email</label>
        <input type="text" name="email" maxlength="15" size="20">
    </p>

    <p>
        <label>password</label>
        <input type="password" name="password" maxlength="15" size="20">
    </p>

    <input type="submit" value="제출">
</form>
</body>
</html>

 

[ membeList.html ]

<!DOCTYPE html>
<!-- 템플릿 엔진(JSP, Thymlear)을 사용-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>회원 목록 조회</title>
</head>
<body>
<div th:replace="member/header :: headerFragment"></div>
<table>
    <tr>
        <th>이름</th>
        <th>이메일</th>
        <th>비밀번호</th>
    </tr>
    <!--반복문으로 객체 개수만큼 row 생성-->
    <tr th:each = "member : ${members}">
        <td th:text="${member.name}"></td>
        <td th:text="${member.email}"></td>
        <td th:text="${member.password}"></td>
    </tr>
</table>

</body>
</html>

 

 

[ 수정 후 코드 ]

header 부분은 통일성 있게 유지되고,

회원등록/회원목록조회를 누름에 따라 아래 화면만 블록 끼우듯이 바뀌는 것을 확인할 수 있다.

 

 

 

 

 

 

 

추가로 회원 가입하면, 회원 목록으로 리다이렉트 하도록 코드를 수정했다.

@PostMapping("members/create")
public String create(MemberRequestDto memberRequestDto){
    System.out.println(memberRequestDto);
    memberService.memberCreate(memberRequestDto);
    return "redirect:/members"; //url 리다이렉트
}

 

 

[ 생성자 주입 방식으로 DI 변경한 코드 ]

 

 

DI, 의존성 주입 방법이란?

https://rookie-programmer.tistory.com/manage/posts/

 

Tistory

좀 아는 블로거들의 유용한 이야기

www.tistory.com

 

 

 

실습

1. responseDto에 id 값 추가 (위에서 추가)

2. Controller에 /member/find?id=xx

3. Service 비즈니스 로직 : Repository 호출

4. Repository에서 실질적으로 data Search -> interface도 수정

5. 상세보기화면 : name, email, password, 가입 시간 출력

 

 

[ MemberRepository.java ]

public Member findById(int id);

 

 

[ MemoryMemberRepository.java ]

@Override
public Member findById(int id) {
    for(Member member : memberDB){
        if(member.getId() == id){
            return member;
        }
    }
    return null;
}

 

 

[ MemberService.java ]

@Service // 싱글톤 컴포넌트로 생성 -> 내부 @Component를 통해 "스프링 빈"으로 등록
// 스프링 빈이랑, 스프링이 생성하고 관리하는 객체를 의미
// 제어의 역전(Inversion of Control)
// IoC 컨테이너가 스프링 빈을 관리한다.(빈 생성, 의존성 주입)
public class MemberService {
    private final MemberRepository memberRepository;
    @Autowired
    public MemberService(MemoryMemberRepository memoryMemberRepository) {
        this.memberRepository = memoryMemberRepository;
    }

    static int total_id;
    public List<MemberResponseDto> members(){
        List<Member> members = memberRepository.members();
        List<MemberResponseDto> memberResponseDtos = new ArrayList<>();
        for(Member member : members){
            MemberResponseDto memberResponseDto = new MemberResponseDto();
            memberResponseDto.setId(member.getId());
            memberResponseDto.setName(member.getName());
            memberResponseDto.setEmail(member.getEmail());
            memberResponseDto.setPassword(member.getPassword());
            memberResponseDto.setCreate_time(member.getCreate_time());
            memberResponseDtos.add(memberResponseDto);
        }
        return memberResponseDtos;
    }

    // 사용자의 입력 값이 담긴 DTO를 통해, 실제 시스템에서 사용되는 정보를 조합해 Member 객체로 변환 후 저장
    public void memberCreate(MemberRequestDto memberRequestDto){
        LocalDateTime now = LocalDateTime.now();
        ++total_id;
        Member member = new Member(total_id, memberRequestDto.getName(), memberRequestDto.getEmail(), memberRequestDto.getPassword(), now);
        memberRepository.memberCreate(member);
    }

    public MemberResponseDto findById(int id){
        //Member 객체를 MemberRequestDto로 변환
        //생성자 초기화보다는 유연성이 좋다.
        Member member = memberRepository.findById(id);
        MemberResponseDto memberResponseDto = new MemberResponseDto();
        memberResponseDto.setId(member.getId());
        memberResponseDto.setName(member.getName());
        memberResponseDto.setEmail(member.getEmail());
        memberResponseDto.setPassword(member.getPassword());
        memberResponseDto.setCreate_time(member.getCreate_time());
        return memberResponseDto;
    }
}

 

 

[ MemberController.java ]

@GetMapping("member/find")
public String findMember(@RequestParam("id") int id, Model model){
    MemberResponseDto memberResponseDto = memberService.findById(id);
    model.addAttribute("member", memberResponseDto);
    return "member/member-find-screen";
}

 

 

[ member-find-screen.html ]

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>회원 상세조회</title>
</head>
<body>
<div th:replace="member/header :: headerFragment"></div>
<table>
    <tr>
        <th>이름</th>
        <th>이메일</th>
        <th>비밀번호</th>
        <th>가입시간</th>
    </tr>
    <tr>
        <td th:text="${member.name}"></td>
        <td th:text="${member.email}"></td>
        <td th:text="${member.password}"></td>
        <td th:text="${member.create_time}"></td>
    </tr>
</table>
</body>
</html>

 

 

 

 

[ 최종 코드 ]