관리 메뉴

bright jazz music

guestbook : 05. DTO를 사용한 Querydsl 테스트 본문

Framework/Spring

guestbook : 05. DTO를 사용한 Querydsl 테스트

bright jazz music 2022. 7. 1. 21:10
실제 프로젝트를 작성하는 경우 엔티티 객체를 persistence context 바깥쪽에서 사용하는 방식보다는 DTO를 이용하는 방식을 권장하였다.

 

  • DTO(Data Transfer Object) : 각 계층끼리 주고받는 우편물 개념. 순수하게 데이터를 담고 있다는 점에서 엔티티 객체와 유사하다. 그러나 목적 자체가 데이터의 전달이므로 읽고 쓰는 것이 모두 허용된다. 또한 persistence context에서 여러 번 사용되는 엔티티 객체와는 달리 일회성으로 사용되는 경향이 있다. 

 

  • 엔티티 객체(Entity): 단순히 데이터를 담는 객체가 아니라 실제 DB와 관련이 있고, 내부적으로 entity manager가 관리하는 객체이다. 엔티티 매니저를 생성하면 그 내부에 일대일로 persistence context가 생성된다. 엔티티 객체는 persistence context 내부에서 관리되는 것이다. 

 

  • 엔티티는 일회성의 DTO와는 달리 생명주기도 다르기 때문에 분리해서 처리하는 것이 권장된다. 이는 마치 웹 어플리케이션 개발 시 HttpServletRequest나 HttpServletResponse를 컨트롤러 계층에서 교환하고, 서비스 계층까지 않는 것과 마찬가지이다. 엔티티 객체는 JPA에서 사용하는 개체이므로 JPA 밖에서는 사용하지 않는 것이 좋다.

 

1. DTO 패키지 및 GuestbookDTO생성 (sampleDTO는 이전 프로젝트에서 사용한 것으로 생성하지 않아도 무방.)

 

 

//GuestbookDTO.class

package com.example.guestbook.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalTime;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GuestbookDTO {

    private Long gno;
    private String title;
    private String writer;
    private LocalTime regDate;
    private LocalTime modDate;

    /*
    * - GuestbookDTO는 GuestbookEntity와 거의 동일한 필드를 가지고 있다.
    * - @Data 어노테이션을 사용해 getter/setter로 값 변경이 가능하도록 구성
    * - 서비스 계층에서 이 DTO를 이용해 필요한 내용을 전달받고 반환한다.
    *
    *   이러한 처리를 가능하도록 하기 위해 service 패키지를 생성하고
    *   GuestbookService와 GuestbookServiceImple 클래스를 작성한다.
    *
    *
    * */
}

 

2. DTO를 사용할 서비스 계층 구성(service 패키지 + service 인터페이스 + serviceImpl 클래스)

service 패키지 생성 +service 인터페이스 생성, serviceImpl 클래스 생성

 

3. 방명록(guestbook) 등록 서비스 처리: GuestbookDTO 이용

 

3-1. 서비스 인터페이스에 사용할 메서드 선언.

//GuestbookService.java (인터페이스)
package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import com.example.guestbook.entity.Guestbook;
import org.springframework.stereotype.Service;

//@Service 어노테이션은 GuestbookImple에 적어준다.
public interface GuestbookService { //GuestbookImple 클래스에서 이 인터페이스를 상속한다.

    Long register(GuestbookDTO dto); // GuestbookImple 클래스에서 오버라이딩
}

 

3-2. 서비스 인터페이스 구현체에서 서비스 인터페이스 상속 및 메서드 오버라이딩

//GuestbookImpl.java

package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service //스프링이 빈으로 처리하도록 @Service 어노테이션 추가
@Log4j2
public class GuestbookServiceImpl implements GuestbookService {

    @Override //GuestbookService에서 상속한 메서드 오버라이딩
    public Long register(GuestbookDTO dto) {
        return null;
    }
}

 

4. 서비스 계층에서 DTO를 Entity로 변환하는 작업 추가하기

4-1. default 메서드를 이용하여 서비스 인터페이스에 구현할 내용(DTO => Entity) 써주기

 

서비스 계층에서는 파라미터를 DTO 타입으로 받는다. 이 DTO를 JPA로 처리하기 위해서는 엔티티 타입의 객체로 변환하는 작업이 필요하다. 이러한 변환 작업은 DTO 클래스에 적용하거나 ModelMapper 라이브러리, 또는 MapStruct 등을 이용한다. 여기서는 수동이 편하다.

 

여기서는 GuestbookService 인터페이스에 default 메서드를 이용해서 이 변환 작업을 처리한다. 가능하면 기존의 DTO와 엔티티 클래스를 변경하고 싶지 않기 때문이다.

 

* default 메서드:
   인터페이스의 실제 내용을 가지는 코드를 default라는 키워드로 생성 가능.
   default 메서드를 이용하면 기존에 추상 클래스를 통해 전달해야 하는 실제 코드를
   인터페이스에 선언할 수 있음.
  
   '인터페이스 -> 추상클래스 -> 구현 클래스'의 형태에서 추상클래스를 생략하는 것이 가능해짐.

 

//GuestbookService.java (인터페이스)
package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import com.example.guestbook.entity.Guestbook;
import org.springframework.stereotype.Service;

//@Service 어노테이션은 GuestbookImple에 적어준다.
public interface GuestbookService { //GuestbookImple 클래스에서 이 인터페이스를 상속한다.

    Long register(GuestbookDTO dto); // GuestbookImple 클래스에서 오버라이딩

    //dto에서 엔티티로의 변환작업
    default Guestbook dtoToEntity(GuestbookDTO dto) {
        Guestbook entity = Guestbook.builder()
                .gno(dto.getGno())
                .title(dto.getTitle())
                .content(dto.getContent())
                .writer(dto.getWriter())
                .build();
        return entity;
    }
}

/*
* default 메서드:
*   인터페이스의 실제 내용을 가지는 코드를 default라는 키워드로 생성 가능.
*   default 메서드를 이용하면 기존에 추상 클래스를 통해 전달해야 하는 실제 코드를
*   인터페이스에 선언할 수 있음.
* 
*   '인터페이스 -> 추상클래스 -> 구현 클래스'의 형태에서 추상클래스를 생략하는 것이 가능해짐.
* */

 

GuestbookService 인터페이스 내에 default 기능을 이용해서 구현 클래스에서 동작 가능한 dtoToEntity() 메서드를 구성하였다. 이를 구현 클래스인 GuestbookServiceImpl 클래스에서 활용하여 파라미터로 전달되는 GuestbookDTO를 엔티티로 변환하자.

 

4-2. 서비스 인터페이스(GuetbookService)의 메서드를 구현 클래스(GuestbookServiceImpl)에  적용하기

//GuestbookImpl.java

package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import com.example.guestbook.entity.Guestbook;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service //스프링이 빈으로 처리하도록 @Service 어노테이션 추가
@Log4j2
public class GuestbookServiceImpl implements GuestbookService {

    @Override //GuestbookService에서 상속한 메서드 오버라이딩
    public Long register(GuestbookDTO dto) {

        log.info("DTO--------------------------------------");
        log.info(dto);
        
        Guestbook entity = dtoToEntity(dto); // GuestbookService에서 default를 이용하여 생성한 메서드
        
        log.info(entity);
        
        return null;
    }
}

 

전달되는 DTO를 Entity로 변환함.

 

 

5. 테스트 코드 작성

GuestbookService를 컨트롤러와 연결하기 전에 기능을 테스트 하기.

 

우선 test 폴더에 service 패키지를 추가하고 GuestbookServiceTests.java를 생성한 뒤 아래와 같이 작성해 준다.

 

//GuestbookServiceTests.java

package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class GuestbookServiceTests {
    @Autowired
    private GuestbookService service;
    
    @Test
    public void testRegister(){
        //테스트 객체 생성
        GuestbookDTO guestbookDTO = GuestbookDTO.builder()
                .title("Sample title...")
                .content("Sample Content...")
                .writer("user0")
                .build();
        
        //service.register() 테스트: 테스트 객체 사용
        System.out.println(service.register(guestbookDTO));
    }
}

 

테스트 결과 성공

그러나 testRegister()의 결과는 실제로 DB에 저장되지는 않는다.

 

6. DB 저장하기

 

  • - JPA 처리를 위해서 GuestbookServiceImpl에 GuestbookRepository를 주입한다.
  • - @RequiredArgsConstructor 어노테이션을 추가한다. (의존성 자동 주입)
  • - register() 메서드 내에 save() 메서드를 사용해서 결과를 저장한다.

그리고 

엔티티의 gno값 리턴

//GuestbookImpl.java

package com.example.guestbook.service;

import com.example.guestbook.dto.GuestbookDTO;
import com.example.guestbook.entity.Guestbook;
import com.example.guestbook.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service //스프링이 빈으로 처리하도록 @Service 어노테이션 추가
@Log4j2
@RequiredArgsConstructor // <== 의존성 자동 주입!
public class GuestbookServiceImpl implements GuestbookService {

    private final GuestbookRepository repository; //jpa 처리를 위해 repository 주입! 반드시 fianl 사용

    @Override //GuestbookService에서 상속한 메서드 오버라이딩
    public Long register(GuestbookDTO dto) {

        log.info("DTO--------------------------------------");
        log.info(dto);

        Guestbook entity = dtoToEntity(dto); // GuestbookService에서 default를 이용하여 생성한 메서드

        log.info(entity);
        
        repository.save(entity); // 처리 저장.
//        return null;
        return entity.getGno(); //처리 후에는 엔티티의 gno 리턴
    }
}

 

그리고 GuestbookServiceTests에서 testRegister() 다시 실행.

 

결과

2022-07-04 10:24:10.107  INFO 18220 --- [    Test worker] c.e.g.service.GuestbookServiceImpl       : DTO--------------------------------------
2022-07-04 10:24:10.131  INFO 18220 --- [    Test worker] c.e.g.service.GuestbookServiceImpl       : GuestbookDTO(gno=null, title=Sample title..., content=Sample Content..., writer=user0, regDate=null, modDate=null)
2022-07-04 10:24:10.156  INFO 18220 --- [    Test worker] c.e.g.service.GuestbookServiceImpl       : Guestbook(gno=null, title=Sample title..., content=Sample Content..., writer=user0)
Hibernate: 
    insert 
    into
        guestbook
        (moddate, reg_date, content, title, writer) 
    values
        (?, ?, ?, ?, ?)
302 // 생성된 행 번호

 

 

302번에 생성. 301번 행도 같은 내용인데, 실수로 테스트를 두 번 했기 때문이다...

 

 

 

 

 

Comments