의존성 추가
---
✅ 유효성 검증을 위한 의존성 추가
- implementation 'org.springframework.boot:spring-boot-starter-validation'
스프링 MVC의 작동방식 이해하기
---
🔗 https://luminousolding.tistory.com/52
DTO의 사용 이유와 역할
---
DTO(Data Transfer Object) 데이터 전송객체
- 로직을 포함하지 않는 순수한 JAVA 객체
- 계층 간 data의 전달이 목적이다.
➡️ DB에서 data를 얻어서 service 계층이나 controller로 전달
- 도메인 대신 사용한다.
➡️ 도메인 사용 시, 모델과 뷰가 강하게 결합하고 뷰가 변화하게 되면 도메인의 코드에도 변화가 생긴다. DTO는 느슨한 결합이 가능하게 한다.
- 캡슐화함으로써 필요한 정보만 전달이 가능하다.
- 유효성 검증이 단순하며 코드가 간결해진다.
🔗 https://www.okta.com/kr/identity-101/dto/
🔗 https://dkswnkk.tistory.com/500
Entity Class
---
간단한 요청 처리 흐름 이해하기
- 클라이언트가 API를 호출하고 요청 데이터를 DTO로 전달
- API 계층은 DTO를 이용해 필요한 데이터를 추출하고 Entity 객체를 생성하거나 업데이트
- API 계층은 생성된 Entity 객체를 서비스 계층으로 전달
- 서비스 계층에서 비즈니스 로직 수행
- API 계층은 Member 객체에서 필요한 정보를 추출해서 DTO로 변환하고 클라이언트에게 응답
✅ Entity Class의 역할
API 계층에서 전달받은 요청 데이터를 기반으로 서비스 계층에서 비즈니스 로직을 처리하기 위해 필요한 데이터를 전달받고, 비즈니스 로직을 처리한 후에는 결과 값을 다시 API 계층으로 리턴해주는 역할
- API 계층(controller) 에서 전달받은 요청 데이터
- 클라이언트가 API를 호출할 때 API 엔트포인트로 전송한 데이터
- HTTP 요청의 본문에 포함되어 전달되며, 클라이언트와 서버 간의 상호작용에서 필요한 정보를 담고 있다.
- 요청 데이터의 형식은 API 디자인 방식에 따라 다르다. (ex. JSON, XML, 폼데이터, CSV 등...)
✅ Entity Class에서 사용되는 애너테이션
- @Getter, @Setter : lombok 라이브러리에서 제공하는 애너테이션, getter/setter 메서드를 직접 작성하지 않아도 되는 편의를 제공한다.
- @AllArgsConstroctor : 클래스에 추가된 모든 변수를 파라미터로 갖는 생성자를 자동으로 생성해준다.
- @NoArgsConstructor : 파라미터가 없는 기본 생성자를 자동으로 생성해준다.
🔗 https://projectlombok.org/features/
Service 계층
---
비즈니스 로직을 수행한다.
- @Service 애너테이션을 추가함으로써 Spring Bean 이 된다.
- 의존성 관리 : 스프링 컨테이너에 의해 관리된다.
- 싱글턴 패턴 : Spring에서 Bean은 컨테이너 내에서 하나의 인스턴스만 생성되고 여러 곳에서 동일한 인스턴스에 접근하여 사용한다.
- 스프링 AOP 적용
- 편리한 설정 관리
- 컨테이너가 객체의 생명주기 관리
🔗 스프링 빈에 대한 소개
https://docs.spring.io/spring-framework/reference/core/beans/introduction.html
Mapper의 역할
---
데이터의 변환작업을 수행한다.
⭐ 코드 변환 작업 전⭐
Controller의 핸들러 메서드가 엔티티 클래스까지 변환할 때와 같은 상황에서 Mapper 클래스가 필요한 것이다.
✅ Controller class
@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
private final MemberService memberService;
public MemberController() {
this.memberService = new MemberService();
}
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = new Member();
member.setEmail(memberDto.getEmail());
member.setName(memberDto.getName());
member.setPhone(memberDto.getPhone());
Member response = memberService.createMember(member);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
}
}
⭐ 코드 변환 작업 후⭐
코드 변환 작업 후 매퍼 클래스를 만들어 줌으로써 역할이 분리 되고 코드의 유지 보수가 쉬워진다.
✅ Mapper class 생성
package co[m.luminousolding.ordersystem.user.mapper;](http://m.luminousolding.ordersystem.user.mapper;)
import co[m.luminousolding.ordersystem.user.dto.UserPostDto;](http://m.luminousolding.ordersystem.user.dto.UserPostDto;)
import co[m.luminousolding.ordersystem.user.entity.User;](http://m.luminousolding.ordersystem.user.entity.User;)
import org.springframework.stereotype.Component;
// Spring bean으로 등록해준다. -> Bean은 controller에서 사용
@Component
public class UserMapper {
// UserPostDto를 User로 변환
public User userPostDtoToUser(UserPostDto userPostDto) {
return new User(0L,
userPostDto.getEmail(),
userPostDto.getNickname(),
userPostDto.getName(),
userPostDto.getPhone());
}
// User를 UserResponseDto로 변환
public UserResponseDto userToUserResponseDto(User user) {
return new UserResponseDto(user.getUserId(),
user.getEmail(),
user.getNickName(),
user.getName(),
user.getPhone());
}
}
✅ MemberResponseDto class 생성
@Getter
@AllArgsConstructor
public class MemberResponseDto {
private long memberId;
private String email;
private String name;
private String phone;
}
✅ UserController 수정
@RestController
@RequestMapping("/v1/users")
@Validated
public class UserController {
private final UserService userService;
private final UserMapper mapper;
public UserController(UserService userService, UserMapper mapper) {
this.userService = userService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postUser(@Valid @RequestBody UserPostDto userPostDto) {
User user = mapper.userPostDtoToUser(userPostDto); // mapper가 원래 작성되어 있던 것을 대신 해줌
User response = userService.createUser(user);
return new ResponseEntity<>(mapper.userToUserResponseDto(response), HttpStatus.CREATED);
}
}
Mapper 의존성 추가
Mapper를 자동 생성하기 위한 의존 라이브러리를 추가 해준다.
dependencies {
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
Mapstruct 기반의 의존 라이브러리를 추가해주고 '@Mapper(componentModel="spring")'을 추가해줌으로서 위의 복잡한 UserMapper또한 간결하게 변경할 수 있다.
package co[m.luminousolding.ordersystem.user.mapper;](http://m.luminousolding.ordersystem.user.mapper;)
import co[m.luminousolding.ordersystem.user.dto.UserPatchDto;](http://m.luminousolding.ordersystem.user.dto.UserPatchDto;)
import co[m.luminousolding.ordersystem.user.dto.UserPostDto;](http://m.luminousolding.ordersystem.user.dto.UserPostDto;)
import co[m.luminousolding.ordersystem.user.dto.UserResponseDto;](http://m.luminousolding.ordersystem.user.dto.UserResponseDto;)
import co[m.luminousolding.ordersystem.user.entity.User;](http://m.luminousolding.ordersystem.user.entity.User;)
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface UserMapper {
User userPostDtoToUser(UserPostDto userPostDto);
User userPatchDtoToUser(UserPatchDto userPatchDto);
UserResponseDto userToUserResponseDto(User user);
}
➕ Mapstruct가 자동으로 생성해준 인터페이스의 구현 클래스는 Gradle의 build task를 실행하면 자동 생성되는데 처음 우리가 구현해준 클래스와 거의 동일한 것을 볼 수 있다.
예외 처리
---
✅ @ExceptionHandler 사용
- 모든 에러 정보를 다 담았을 때
@Getter
@AllArgsConstructor
public class ErrorResponse {
private List fieldErrors;
@Getter
@AllArgsConstructor
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
}
}
ErrorResponse를 List에 담는 이유 : DTO 클래스에서 검증해야 되는 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상 될 수 있기 때문
- 필요한 정보만 골라서 Response에 전달
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
final List fieldErrors = e.getBindingResult().getFieldErrors();
// 추가된 부분
List errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity(new ErrorResponse(errors), HttpStatus.BAD\_REQUEST); // response 수정
}
**➡️ controller 에서 @ExceptionHandler 애너테이션 사용시 문제점**
- 코드 중복 : 다른 controller 에서도 동일한 방식의 예외처리코드가 중복된다.
- 유연성 : 다른 예외 발생 시, 예외를 또 만들어 줘야 한다.
✅ @RestControllerAdvice
🔗 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
- @ResponseBody + @ControllerAdvice 의 기능을 수행한다.
- @ControllerAdive
- controller (전역)에서 발생할 수 있는 예외를 잡아주는 애너테이션
- RestControllerAdvice는 RESTful 한 ControllerAdvice라고 생각하면 된다.
-> 따라서 응답 또한 ResponseEntity 로 래핑이 아닌 ErrorResponse로 바로 리턴이 가능하다.
⭐ GlobalExceptionAdvice & ErrorResponse 클래스 참고 ⭐
✅ 의도적인 예외 던지기/받기 (throw/catch)
- GlobalExceptionAdvice 에 RuntimeException을 잡아서 처리하기 위한 handleResourceNotFoundException() 메서드 추가
- MemberController의 getMember() 핸들러 메서드에 요청
- MemberService에서 RuntimeException 예외 던지기
- GlobalExceptionAdvice의 handleResourceNotFoundException() 메서드가 이 RuntimeException을 잡아서 예외 메시지인 “Not found member”를 콘솔에 출력
**➡️ handleResourceNotFoundException() 메서드의 문제점**
의도적으로 던질 수 있는 예외는 다양하다. 하지만 RuntimeException()을 넘기게 되면 어떤 예외를 발생시켰는지 명확하게 알기가 힘들다.
- 회원 정보 없을 때
- 회원 등록 정보가 이미 있을 때
- 로그인 패스워드 검증에서 패스워드가 일치하지 않을 때 등
🔗 https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html
✅ 사용자 정의 예외(Custom Exception) 사용하기
⭐ exception.ExceptionCode ⭐
서비스 계층에 던질 예외 코드를 enum으로 직접 정의 해준다.
- 다양한 유형의 예외를 직접 정의해서 필요한 예외를 선택해서 사용이 가능하다.
⭐ exception.BusinessLogicException ⭐
RuntimeException을 상속하는 BusinessLogicException
- ExceptionCode 를 멤버 변수로 사용
- 더욱 구체적인 예외 정보를 제공한다.
- 상위 클래스인 RuntimeException의 생성자(super)로 메시지를 전달한다.
- getMessage이외에도 원하는 정보를 선택해서 던질 수 있음
➕ 'GlobalExceptionAdvice' 클래스에 서비스 계층의 비즈니스 로직 처리 발생 예외를 처리하는 handleBuisnessLogicException()메서드 생성
> controller ▶️ 서비스 계층 (비즈니스 로직 처리) 에서 예외 발생 ▶️ GlobalExceptionAdvice의 handleBusinessLogicException() 메서드▶️ BusinessLogicException 처리
결과로 콘솔창에 enum 에 만들어준 예외 메세지가 나오게 된다.
ex) 404 ** Not Found