관리 메뉴

거니의 velog

(8) 스프링 부트와 API 서버 3 본문

SpringBoot_React 풀스택 프로젝트

(8) 스프링 부트와 API 서버 3

Unlimited00 2024. 2. 28. 12:54

3. 서비스 계층과 DTO 처리

* 엔티티 객체는 단순한 자바의 인스턴스가 아니라 JPA를 통해서 관리되고 있는 객체(영속 객체)이다. 따라서 실제 데이터를 서비스할 때는 엔티티 객체의 내용물을 복사해서 사용하는 DTO를 이용한다.
* 프로젝트 내에서 dto 패키지를 생성하고 TodoDTO를 생성한다.

package com.unlimited.mallapi.dto;

import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonFormat;

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

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

    // 할일 번호
    private Long tno;
    // 할일 제목
    private String title;
    // 작성자
    private String writer;
    // 완료 여부
    private boolean complete;

    // 마감일, JSON 형식 지정
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    private LocalDate dueDate;

}

* TodoDTO는 Lombok의 기능을 활용해서 getter/setter를 생성하고 날짜는 화면에서 쉽게 처리하도록 @JsonFormat을 이용해서 '2024-02-28' 과 같은 포맷으로 구성한다.


(1) 서비스 선언

* 서비스 계층은 DTO 타입으로 데이터를 주고 받도록 구성한다. service 패키지를 구성하고 TodoService.java 인터페이스와 TodoServiceImpl.java 클래스를 추가한다.

* TodoService 인터페이스에는 등록 기능을 선언한다. 등록 기능은 반환값으로 새로 등록된 Todo의 번호를 반환하도록 한다.

package com.unlimited.mallapi.service;

import com.unlimited.mallapi.dto.TodoDTO;

public interface TodoService {

    Long register(TodoDTO todoDTO);

}

* TodoServiceImpl은 TodoService 인터페이스의 구현체로 아직 내용은 없이 구성만을 추가한다.

package com.unlimited.mallapi.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.dto.TodoDTO;

import lombok.extern.log4j.Log4j2;

@Service
@Transactional(rollbackFor = Exception.class)
@Log4j2
public class TodoServiceImpl implements TodoService {

    @Override
    public Long register(TodoDTO todoDTO) {
        log.info("............");

        return null;
    }

}

(2) ModelMapper 라이브러리

* 서비스 계층의 파라미터와 리턴 타입은 DTO를 이용하지만 내부적으로는 엔티티 객체를 사용해야 하는 경우가 많기 때문에 'DTO <-> 엔티티' 처리를 수월하게 할 수 있는 ModelMapper 를 활용하는 것이 편리하다.
* build.gradle 파일에 ModelMapper 라이브러리를 추가한다.

dependencies {
    (...)
    implementation 'org.modelmapper:modelmapper:3.1.1'
}
[NOTE]

* VSCode에서는 라이브러리를 추가한 후에는 'Clean.Server Workspace'를 해주는 것이 안전하다.

* 프로젝트에는 config 패키지를 추가하고 RootConfig.java 파일을 추가한다.

* RootConfig 는 스프링에서 설정 파일의 역할을 하는 @Configuration 어노테이션을 추가하고 ModelMapper를 설정한다.

package com.unlimited.mallapi.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RootConfig {

    // ModelMapper 빈 등록
    @Bean
    public ModelMapper getMapper() {
        // ModelMapper 객체 생성
        ModelMapper modelMapper = new ModelMapper();
        
        // ModelMapper 설정
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)  // 필드 매칭 활성화
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)  // 필드 접근 레벨 설정
                .setMatchingStrategy(MatchingStrategies.LOOSE);  // 매칭 전략 설정

        // 생성한 ModelMapper 반환
        return modelMapper;
    }

}

4. 서비스 계층의 구현

* 서비스 계층의 구현은 TodoDTO 타입으로 파라미터나 리턴 타입을 처리한다. 그리고 TodoRepository로 Todo 엔티티 객체를 처리해야 하기 때문에 ModelMapper로 간단하게 처리하는 방법을 사용한다.


(1) 등록 기능의 구현

* TodoServiceImpl의 등록 기능은 다음과 같이 작성된다. 서비스 객체를 구성할 때는 항상 트랜잭션 처리를 설정해 두고 작업해야 한다.

package com.unlimited.mallapi.service;

import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.domain.Todo;
import com.unlimited.mallapi.dto.TodoDTO;
import com.unlimited.mallapi.repository.TodoRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@Transactional(rollbackFor = Exception.class)
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class TodoServiceImpl implements TodoService {

    // ModelMapper와 TodoRepository를 생성자 주입
    private final ModelMapper modelMapper;     // DTO와 Entity 간 변환을 위한 ModelMapper
    private final TodoRepository todoRepository;  // 할일 데이터 처리를 위한 TodoRepository

    // 할일 등록 메서드
    @Override
    public Long register(TodoDTO todoDTO) {
        log.info("할일 등록 서비스 호출");

        // TodoDTO를 Todo 엔티티로 변환
        Todo todo = modelMapper.map(todoDTO, Todo.class);

        // TodoRepository를 통해 할일 저장
        Todo savedTodo = todoRepository.save(todo);

        // 저장된 할일의 번호 반환
        return savedTodo.getTno();
    }

}

* test 폴더에는 service 패키지를 추가하고 TodoServiceTests.java 파일을 추가해서 작성된 TodoService의 테스트를 진행한다.

package com.unlimited.mallapi.service;

import java.time.LocalDate;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.unlimited.mallapi.dto.TodoDTO;

import lombok.extern.log4j.Log4j2;

@SpringBootTest
@Log4j2
public class TodoServiceTests {

    // TodoService 주입
    @Autowired
    private TodoService todoService;

    // 할일 등록 테스트
    @Test
    public void testRegister() {
        // 할일 DTO 생성
        TodoDTO todoDTO = TodoDTO.builder()
                .title("서비스 테스트")                   // 할일 제목 설정
                .writer("tester")                        // 작성자 설정
                .dueDate(LocalDate.of(2024, 2, 28))      // 마감일 설정
                .build();

        // 할일 등록 서비스 호출
        Long tno = todoService.register(todoDTO);

        // 등록된 할일의 번호 출력
        log.info("tno : {}", tno);
    }

}

* 테스트 코드를 실행해서 새로운 번호와 Todo 데이터가 생성되는지 확인하고 데이터베이스에서도 결과를 확인해야 한다.


(2) 조회 기능의 구현

* TodoService에는 TodoDTO를 반환하는 조회용 메서드를 추가한다.

package com.unlimited.mallapi.service;

import com.unlimited.mallapi.dto.TodoDTO;

public interface TodoService {

    Long register(TodoDTO todoDTO);

    TodoDTO get(Long tno);

}

* TodoServiceImpl 에서의 구현은 Todo 엔티티 객체를 구하고 이를 ModelMapper를 이용해 TodoDTO로 변환해서 반환한다.

package com.unlimited.mallapi.service;

import java.util.Optional;

import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.domain.Todo;
import com.unlimited.mallapi.dto.TodoDTO;
import com.unlimited.mallapi.repository.TodoRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@Transactional(rollbackFor = Exception.class)
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class TodoServiceImpl implements TodoService {

    // ModelMapper와 TodoRepository를 생성자 주입
    private final ModelMapper modelMapper;     // DTO와 Entity 간 변환을 위한 ModelMapper
    private final TodoRepository todoRepository;  // 할일 데이터 처리를 위한 TodoRepository

    // 할일 등록 메서드
    @Override
    public Long register(TodoDTO todoDTO) {
        log.info("할일 등록 서비스 호출");

        // TodoDTO를 Todo 엔티티로 변환
        Todo todo = modelMapper.map(todoDTO, Todo.class);

        // TodoRepository를 통해 할일 저장
        Todo savedTodo = todoRepository.save(todo);

        // 저장된 할일의 번호 반환
        return savedTodo.getTno();
    }

    // 특정 번호의 할일 조회 메서드
    @Override
    public TodoDTO get(Long tno) {
        // TodoRepository를 통해 특정 번호의 할일 조회
        // import java.util.Optional;
        Optional<Todo> result = todoRepository.findById(tno);

        // 조회된 할일이 없으면 예외를 던짐
        Todo todo = result.orElseThrow();

        // 조회된 할일을 TodoDTO로 변환
        TodoDTO dto = modelMapper.map(todo, TodoDTO.class);

        // 변환된 TodoDTO 반환
        return dto;
    }

}

* 테스트 코드에서 현재 데이터베이스에 있는 번호로 확인해 보자.

package com.unlimited.mallapi.service;

import java.time.LocalDate;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.unlimited.mallapi.dto.TodoDTO;

import lombok.extern.log4j.Log4j2;

@SpringBootTest
@Log4j2
public class TodoServiceTests {

    // TodoService 주입
    @Autowired
    private TodoService todoService;

    // 할일 등록 테스트
    @Test
    public void testRegister() {
        // 할일 DTO 생성
        TodoDTO todoDTO = TodoDTO.builder()
                .title("서비스 테스트")                   // 할일 제목 설정
                .writer("tester")                        // 작성자 설정
                .dueDate(LocalDate.of(2024, 2, 28))      // 마감일 설정
                .build();

        // 할일 등록 서비스 호출
        Long tno = todoService.register(todoDTO);

        // 등록된 할일의 번호 출력
        log.info("tno : {}", tno);
    }

    // 특정 번호의 할일 조회 테스트
    @Test
    public void testGet() {
        // 조회할 할일 번호 설정
        Long tno = 101L; // 방금 테스트 코드로 등록한 101번을 조회해 보자.

        // 할일 조회 서비스 호출
        TodoDTO todoDTO = todoService.get(tno);

        // 조회된 할일 정보 출력
        log.info(todoDTO);
    }

}

(3) 수정/삭제 기능의 구현

* TodoService에 수정 기능과 삭제 기능을 선언한다.

package com.unlimited.mallapi.service;

import com.unlimited.mallapi.dto.TodoDTO;

public interface TodoService {

    Long register(TodoDTO todoDTO);

    TodoDTO get(Long tno);

    void modify(TodoDTO todoDTO);

    void remove(Long tno);

}

* TodoServiceImpl에서 수정 기능의 구현은 기존의 Todo를 먼저 로딩한 후에 TodoDTO에서 변경된 내용(title, complete, dueDate)을 반영하고 다시 저장하는 방식으로 구현된다.

package com.unlimited.mallapi.service;

import java.util.Optional;

import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.domain.Todo;
import com.unlimited.mallapi.dto.TodoDTO;
import com.unlimited.mallapi.repository.TodoRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@Transactional(rollbackFor = Exception.class)
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class TodoServiceImpl implements TodoService {

    // ModelMapper와 TodoRepository를 생성자 주입
    private final ModelMapper modelMapper;     // DTO와 Entity 간 변환을 위한 ModelMapper
    private final TodoRepository todoRepository;  // 할일 데이터 처리를 위한 TodoRepository

    // 할일 등록 메서드
    @Override
    public Long register(TodoDTO todoDTO) {
        log.info("할일 등록 서비스 호출");

        // TodoDTO를 Todo 엔티티로 변환
        Todo todo = modelMapper.map(todoDTO, Todo.class);

        // TodoRepository를 통해 할일 저장
        Todo savedTodo = todoRepository.save(todo);

        // 저장된 할일의 번호 반환
        return savedTodo.getTno();
    }

    // 특정 번호의 할일 조회 메서드
    @Override
    public TodoDTO get(Long tno) {
        // TodoRepository를 통해 특정 번호의 할일 조회
        // import java.util.Optional;
        Optional<Todo> result = todoRepository.findById(tno);

        // 조회된 할일이 없으면 예외를 던짐
        Todo todo = result.orElseThrow();

        // 조회된 할일을 TodoDTO로 변환
        TodoDTO dto = modelMapper.map(todo, TodoDTO.class);

        // 변환된 TodoDTO 반환
        return dto;
    }

    // 할일 수정 메서드
    @Override
    public void modify(TodoDTO todoDTO) {
        // TodoRepository를 통해 수정할 할일 조회
        Optional<Todo> result = todoRepository.findById(todoDTO.getTno());

        // 조회된 할일이 없으면 예외를 던짐
        Todo todo = result.orElseThrow();

        // 할일 정보 변경
        todo.changeTitle(todoDTO.getTitle());
        todo.changeDueDate(todoDTO.getDueDate());
        todo.changeComplete(todoDTO.isComplete());

        // 변경된 할일을 TodoRepository를 통해 저장
        todoRepository.save(todo);
    }

    // 할일 삭제 메서드
    @Override
    public void remove(Long tno) {
        // TodoRepository를 통해 할일 삭제
        todoRepository.deleteById(tno);
    }

}

* 앞의 기능들과 동일하게 테스트 코드를 통해서 동작 여부를 확인해 두는 것이 좋다.



[수정 기능 테스트]

    @Test
    public void testModify() {
        Long tno = 101L;
        TodoDTO getResult = todoService.get(tno);
        log.info(getResult);

        if (getResult != null) {
            log.info("수정할 데이터가 있어유~");
        }
    }
    @Test
    public void testModify() {
        Long tno = 101L;
        TodoDTO getResult = todoService.get(tno);
        log.info(getResult);

        if (getResult != null) {
            log.info("수정할 데이터가 있어유~");

            TodoDTO todoDTO = TodoDTO.builder()
                    .tno(tno)
                    .title("수정된 제목이어유~ 일단 ㄱㄱ")
                    .writer(getResult.getWriter())
                    .dueDate(LocalDate.of(2023, 12, 12))
                    .complete(true)
                    .build();

            todoService.modify(todoDTO);

            log.info(todoDTO);
        }
    }


[삭제 기능 테스트]

    @Test
    public void testDelete() {
        Long tno = 101L;
        TodoDTO getResult = todoService.get(tno);
        log.info(getResult);

        if (getResult != null) {
            todoService.remove(tno);
        }
    }
101번 게시물이 삭제되었다.