본문 바로가기

코딩/Java, SpringBoot

presentation layer, service layer 명확히 구분 짓는 코드

JPA김영한님의 책을 읽고 있다. 13장에서 프리젠테이션, 서비스, 레포지토리 레이어의 분리와 역할침범에 대해서 많이 나온다. 고민하다가 내가 쓴 코드가 꽤나 문제를 해결해주는 것 같아서 블로그로 남기고자 한다.


먼저 레이어간 침범이 일어나는 경우가 뭔지 알아보자(JPA 책 584p에 잘 설명되어 있다.)

  1. 뷰에서 필요한 정보에 따라 다른 서비스 레이어 메소드가 필요한 경우. 뷰까지는 영속성 컨텍스트와 트랜잭션이 살아있지 않기 때문에 지연로딩이 불가능하다. 지연로딩이 불가능하기 때문에 프록시 객체를 강제 초기화 하는 방법을 사용하면 뷰마다 어떤 정보가 필요한지 알고 메소드마다 같은 기능을 하더라도 어떤 메소드는 프록시 객체를 강제 초기화. 어떤 메소드는 초기화 하지 않고 반환하게 된다. 최적화를 위해서라면 당연한 절차이다. 이런 상황이 프리젠테이션 레이어가 서비스 레이어를 침범하는 상황이다. 
  2. 만약 강제 초기화가 아니라 JPQL fetch join을 사용해서 문제를 해결하려 한다면 필요한 뷰에 최적화된 JPQL을 작성하게 된다. 어떤 뷰는 join이 필요해서 fetch join이 포함된 레포지토리 메소드를 실행하고 어떤 뷰는 join이 필요하지 않아서 fetch join이 포함되지 않은 메소드를 실행할 것이다. 이런 상황이 프리젠테이션 레이어가 레포지토리 레이어를 침범하는 상황이다.

내가 제시하고자 하는 것은 프리젠테이션 - 서비스 레이어간의 분리를 좀더 잘 할 수 있는 코드, 구조이다.

나는 서비스 레이어의 모든 함수에 Function 메소드를 매개변수로 받아서 entity->DTO 변환하는 함수를 넣도록 했다. 렇게 하면 뷰에서 필요한 정보마다 다르게 서비스 레이어의 메소드를 짜야하는 문제를 벗어날 있다. 프리젠테이션 레이어에서 자신이 필요한 DTO클래스를 정하고 변환함수를 넘기면 된다. 어떤 정보기 필요한지 프리젠테이션 레이어에서 정하므로 좀더 레이어의 역할이 잘 분리되었다고 할 수 있다.

 

서비스 레이어에서는 비즈니스 로직을 선택하고 컨트롤러가 요청한 DTO를 반환할 수 있도록 apply()메소드를 실행해서 변환후 넘겨주기만 하면 된다.

서비스 레이어는 컨트롤러에서 어떤 정보가 필요하고 어떤 DTO를 요청했는지 알 필요가 없다. 컨트롤러와 서비스 레이어의 결합, 침범을 줄이는 코드라고 생각된다.

 

//서비스 레이어 코드

public <T> T findById(Function<Store, T> func, UUID uuid) {
		Optional<Store> store = storeRepository.findById(uuid);//없으면 null
		T res = null;
		if (store.isPresent()) {
			res = func.apply(store.get());
		}
		return res;
	}

 

@GetMapping("")//컨트롤러 코드
	public StoreEditDTO oneStore(@RequestParam UUID id) {
		//store id로 cake모두 찾기
		List<CakeEditDTO> cakeEditDTOList = cakeService.findByStoreUUID(CakeEditDTO::toDTO, id);
		//store 찾기.
		StoreEditDTO storeEditDTO = storeService.findById(i -> StoreEditDTO.toDTO(i), id);
		//cakeList연결
		storeEditDTO.setCakeList(cakeEditDTOList);//cakeList는 toDTO에서 처리 못하기 때문에 따로 설정해줘야 한다.
		return storeEditDTO;
	}

 

 

아래와 같이 DTO클래스에 변환함수를 만들어 주면 된다. static으로 만드는게 중요하다.! 그리고 아래와 같이 entity-DTO변환 메소드를 작성할 경우 DTO를 만들 때 연관된 엔티티가 필요하다면 엔티티의 getter를 불러와서 DTO를 만들기 때문에 자연스레 프록시 초기화하고 값을 가져온다. 

public class StoreEditDTO {
    private UUID uuid;
    private String mainImg; //base64인코딩 된 상태.
    private String extension; //byte파일의 확장자.

    private String name;//가게이름.

    private String description;

//    private String impossibleDate;

    private List<CakeEditDTO> cakeList;
    
    public static StoreEditDTO toDTO(Store s) {//entity - DTO 변환메소드
    
        Path path = Paths.get(s.getFilePath());
        String bytes=null;
        try {
            bytes = Base64.getEncoder().encodeToString(Files.readAllBytes(path));//항상 까먹지말자!
        } catch (IOException e) {
            e.printStackTrace();
            log.warn("파일이 존재하지 않습니다. : " + path.toAbsolutePath());
        }
        String fileName = path.getFileName().toString();
		int i = fileName.lastIndexOf(".");//파일이름에.가 들어갈 수 있기 때문에 lastIndexOf
		String extension = fileName.substring(i + 1);
    
        StoreEditDTO editDTO = StoreEditDTO.builder()
                .uuid(s.getUuid())
                .name(s.getName())
                .mainImg(bytes)
                .description(s.getDescription())
                .extension(extension)
                .build();
        return editDTO;
    }

}