


문제발생 : 기존 Calendar 는 모든 API를 RequestDto, ResponseDto 단 2개로 관리를 진행했음. DTO가 Entity형태를 띄면서모든레이어를 관통. 해당 문제는 API별로 Dto를 새로생성하면 문제가 되지않지만 레이어를 관통하는문제는 그대로임. 또한 기존의 Entity가 DTO를 받아서 변환하면 Entity가 DTO를 의존하는 문제또한 발생.
원인 : 기존의 Service 는 RequestDto 를 바로 받아 바로 Entity로 변환해줬음. 이렇게 진행이됄경우 정보에대한 유효성과 변환등을 Entity가 책임을 져야하기에 Entity Class 에 필요이상이 책임이 들어간다고 판단.
가설 : DTO를 레이어단위에 종속시키게 만드는 기법 혹은 코딩방법이 있지않을까?
문제해결 : 서비스레이어 앞단에 정적유틸클래스인 Command 와 끝단에 Mapper를 두어서 Entity의 책임과 관심사를 분리.
데이터 검증과 변환은 Command가 책임을 질수있게끔 흐름을 만들어주었음. Command 내부에선 StaticInner Class 를 통해 기능별 필요한 필드만을 받아서 Entity로 변환을 이루어준다.
이로 인해 Entity나 DTO가 수정되더라도 Command 혹은 Mapper만 수정하면된다.
생각해볼 점 : 정적유틸클래스 Command를 통해 컨트롤러와 서비스레이어의 의존도는 내려갔지만, DTO가 계층을 관통을 하는 문제는 여전하다. 하지만 컨트롤러와 서비스가 원하는 관심사가 크게 다르지않을때 이 문제가 정말로 문제인가의 관점이 더 중요하다고 생각이됌.
이 문제로 여러튜터님들에게 질문을 해봤을땐 일반적인 상황에선 잘발생하지않는 문제로 서비스 DTO가 존재했을때의 유지보수적인 관점등에선 오히려 손해가 아닐까 라는 답변을 받기도했다. 차라리 필드의 갯수가 적다면 RequestDto를 그대로 서비스로 보내는게 아닌 필드자체를 분해해서 파라미터로 보내는게 좀더 직관적인일수도 있다고 느낌.
public ScheduleCreateResponseDto createSchedule(ScheduleCreateRequestDto scrDto) {
Schedule schedule = ScheduleCommand.Create.toEntity(scrDto);
scheduleRepository.save(schedule);
return scheduleMapper.scheduleToScheduleCreateResponseDto(schedule);
}
public class ScheduleCommand {
public static class Create {
private static String userName;
private static String title;
private static String scheduleDetails;
public static Schedule toEntity(ScheduleCreateRequestDto scrDto) {
userName = scrDto.getUserName();
title = scrDto.getTitle();
scheduleDetails = scrDto.getScheduleDetails();
return Schedule.builder()
.userName(userName)
.title(title)
.scheduleDetails(scheduleDetails)
.build();
}
}
@Mapper(componentModel = "spring")
public interface ScheduleMapper {
ScheduleCreateResponseDto scheduleToScheduleCreateResponseDto(Schedule schedule);

편하게 가져올수있는 방법이 뭐가없을까?@Mapper(componentModel = "spring")
public interface ScheduleMapper {
ScheduleCreateResponseDto scheduleToScheduleCreateResponseDto(Schedule schedule);
ScheduleReadResponseDto scheduleToScheduleReadResponseDto(Schedule schedule);
ScheduleReadPageResponseDto scheduleToScheduleReadPageResponseDto(Schedule schedule);
}
온전히 Entity 필드와 동일하게 존재하는 상황이면 문제가없지만 ResponseDto 내부에서 Entity에 없는 필드값을 요구할때도있다. @Mapping(source = "schedule.id", target = "scheduleId")
CommentCreateResponseDto commentToCommentCreateResponseDto(Comment comment);
@Mapping 어노테이션을 활용해 Entity 내부의 명시적으로 없는 필드값 또한 받아올수있게 지원을 하고있기에 이를 활용해 문제를 해결하였다.

일정 : 유저 가 N : M 관계로 지정되면서 의도하지 않아도 중간테이블이 발생.N : M 관계인 @ManyToMany 어노테이션을 사용하게되면 자동적으로 중간테이블을 작성해서 관리한다.@ManyToOne(fetch = FetchType.LAZY) 를 추가해 필요할때만 쿼리문을 요청해 성능을 향상.
@Setter
@Getter
@Entity
@Table(name = "userschedule")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_id")
private Schedule schedule;
@Column(name = "role", nullable = false)
private String role;
private LocalDateTime joinedAt;
public boolean isValidateCreator(Long creatorId) {
if (!this.schedule.getUser().getId().equals(creatorId)) {
return true;
}
return false;
}
}
public ScheduleCreateResponseDto createSchedule(User user, ScheduleCreateRequestDto scrDto) {
Schedule schedule = ScheduleCommand.Create.toSchedule(scrDto, user);
scheduleRepository.save(schedule);
UserSchedule userSchedule = ScheduleCommand.Create.toUserSchedule(schedule);
userScheduleRepository.save(userSchedule);
return scheduleMapper.scheduleToScheduleCreateResponseDto(schedule);
}
public static UserSchedule toUserSchedule(Schedule schedule) {
return UserSchedule.builder()
.user(schedule.getUser())
.schedule(schedule)
.role("creator")
.joinedAt(LocalDateTime.now())
.build();
}

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({CustomException.class})
protected ResponseEntity<ErrorDto> handleCustomException(CustomException e, HttpServletRequest req) {
log.error("url:{}, trace:{}", req.getRequestURI(), e.getStackTrace());
return new ResponseEntity<>(new ErrorDto(e.getErrorCode().getCode(), e.getErrorCode().getDescription()), HttpStatus.valueOf(e.getErrorCode().getCode().value()));
}
@ExceptionHandler({MethodArgumentNotValidException.class})
protected ResponseEntity<ErrorDto> handleValidationException(MethodArgumentNotValidException e, HttpServletRequest req) {
log.error("url:{}, trace:{}", req.getRequestURI(), e.getStackTrace());
return new ResponseEntity<>(new ErrorDto((HttpStatus) e.getStatusCode(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage()), HttpStatus.valueOf(e.getStatusCode().value()));
}
}
if 문을 통해 하나하나 바인딩 해주던 이전과 다르게 Exception Class 의 메소드를 최대한 활용하여 값을 바인딩, 코드 수를 줄이고 가독성 증대.
기존 처럼 예외마다 같은 HttpStatus 를 보내는게 아닌 알맞은 에러 코드를 전송.


참조 :
https://velog.io/@kiiiyeon/%EC%8A%A4%ED%94%84%EB%A7%81-ExceptionHandler%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC
https://hocheon.tistory.com/68

@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
@RequiredArgsConstructor
public class AuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
if (isFilterApplicable(req)) {
chain.doFilter(req, res);
}
String tokenValue = jwtUtil.getTokenFromRequest(req);
if (!StringUtils.hasText(tokenValue)) {
throw new CustomException(TOKEN_NOT_FOUND);
}
String accessToken = jwtUtil.substringToken(tokenValue);
if (!jwtUtil.validateToken(accessToken)) {
throw new CustomException(INVALID_TOKEN);
}
chain.doFilter(req, res);
}
private boolean isFilterApplicable(HttpServletRequest req) {
String path = req.getRequestURI();
return path.startsWith("/user/registration") || path.startsWith("/user/login");
}
}
@Order(0)
@Component
public class AuthenticationExceptionHandlerFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
try {
chain.doFilter(req, res);
} catch (CustomException e) {
if (!res.isCommitted()) {
res.setStatus(e.getErrorCode().getStatus());
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
res.getWriter().write(e.getErrorCode().toString());
}
}
}
}
Spring MVC 안에서 작동되는 게 아닌 Dispatcher-Servlet 앞단에서 실행되기에 기존의 @RestControllerAdvice 을 활용한 ExceptionHandler 는 동작하지않는다.앞 단 에 필터의 예외를 잡아주는 ExceptionHandlerFilter Handler를 하나 두어서 필터의 예외 가 생겼을 때 예외를 잡아주게 설정. 

참조 :
https://velog.io/@kimdy0915/Spring-Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EA%B9%8C
https://velog.io/@lionjojo/JWT-%EC%84%B8%EC%85%98

인증/인가 가 필요한 API들은 JWT 토큰의 담겨져 있는 UserId(PK) 와 권한을 요청마다 가져와 User Entity 객체를 가져와 사용하고있다. 이로인해 요청마다 User를 DB에서 가져오는 공통코드가 발생.HandlerMethodArgumentResolver 를 통해 이를 해결할수 있다. HandlerMethodArgumentResolver 는 컨트롤 메서드의 파라미터중 특정 조건에 맞는 파라미터가 있다면, 요청값을 이용해 원하는 객체를 만들어 바인딩이 가능하다.@Component
@RequiredArgsConstructor
public class LoginUserResolver implements HandlerMethodArgumentResolver {
private final UserService userService;
private final JwtUtil jwtUtil;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasLoginUserAnnotation = parameter.hasParameterAnnotation(LoginUser.class);
boolean isUserType = User.class.isAssignableFrom(parameter.getParameterType());
return hasLoginUserAnnotation && isUserType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest req = (HttpServletRequest) webRequest.getNativeRequest();
String token = jwtUtil.getTokenFromRequest(req);
if (!StringUtils.hasText(token)) {
throw new CustomException(TOKEN_NOT_FOUND);
}
token = jwtUtil.substringToken(token);
Claims claims = jwtUtil.getUserInfoFromToken(token);
token = claims.getSubject();
return userService.getLogInUser(token);
}
}
supportsParameter : 해당 메소드의 매개변수가 해당 resolver가 지원하는지 체크.
@LogingUser 이 붙어있는지, 매개변수 타입이 User 인지 체크한다.resolveArgument : 매개변수로 넣어 줄 값을 제공하는 메소드

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginUserResolver loginUserResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserResolver);
}
}
해당 resolver를 사용하기 위해선 WebMvcConfigurer 의 구현체가 필요하다.
해당 구현체를 통해 Bean 으로 등록한 resolver를 주입받아서 사용한다.
@PostMapping()
public ResponseEntity<ScheduleCreateResponseDto> createsSchedule(@LoginUser User user, @Valid @RequestBody ScheduleCreateRequestDto scrDto) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(scheduleService.createSchedule(user, scrDto));
}
참조 :
https://breakcoding.tistory.com/402
https://velog.io/@uiurihappy/Spring-Argument-Resolver-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9C%A0%EC%A0%80-%EC%A0%95%EB%B3%B4-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0
문제라기보단 하나의 고찰이라고 보는게 맞을것 같음.
Service -> Service 의 의존관계가 발생.Service -> Service 의 형태가 문제되지않음.public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final UserScheduleRepository userScheduleRepository;
private final ScheduleMapper scheduleMapper;
private final WeatherService weatherService;
public ScheduleCreateResponseDto createSchedule(User user, ScheduleCreateRequestDto scrDto) {
Schedule schedule = ScheduleCommand.Create.toSchedule(scrDto, user, weatherService);
scheduleRepository.save(schedule);
UserSchedule userSchedule = ScheduleCommand.Create.toUserSchedule(schedule);
userScheduleRepository.save(userSchedule);
return scheduleMapper.scheduleToScheduleCreateResponseDto(schedule);
}
public class ScheduleCommand {
public static class Create {
private static String title;
private static String scheduleDetails;
private static String weather;
public static Schedule toSchedule(ScheduleCreateRequestDto scrDto, User user, WeatherService weatherService) {
title = scrDto.getTitle();
scheduleDetails = scrDto.getScheduleDetails();
weather = weatherService.getWeather();
return Schedule.builder()
.user(user)
.title(title)
.scheduleDetails(scheduleDetails)
.weather(weather)
.build();
}
현재 프로젝트에서 사용하고 있는 방식으로 일정 Service -> 날씨 Client 의 구조를 지니지만, 반대의 형식인 날씨 Client -> 일정 Service 의 형태를 이루고있지 않기에 순환참조가 이뤄지지않는다 판단하여 사용.
@RestController
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
private final TestOtherService testOtherService;
@GetMapping(value = "/test")
public void test() {
// testService a 메소드 호출
testService.a();
// testOtherService b 메소드 호출
testOtherService.b();
}
}
해당 방식은 컨트롤러가 어느 Service 를 사용하는지 개발자가 파악을하기 편하지만 가장 큰문제가 존재한다.
트랜잭션의 원자성 을 지키지못하는게 가장 큰문제로 각각의 Service Method 인 a(),b() 가 각각의 트랜잭션의 단위에서 실행되기에 모두 실행되거나 아예 실행되지않아야하는 트랜잭션의 원자성을 보장하지 못한다.

해당 항목을 작성한 이유 중 하나로 여러개의 낮은 추상화 인터페이스를 하나의 고수준 인터페이스로 묶어주는 디자인 패턴중 하나다.
파사드패턴을 통해 시스템을 모듈화하고 높은 추상화의 단계로 하나의 인터페이스로 여러개의 인터페이스를 조종할 수 있다.
단점으론 파사드 클래스가 사용할 기능만을 노출시키기에 모든 기능을 제공하지 않을수도 있다. 또한 다수의 서브인터페이스와 동작함으로 오버헤드 발생가능성이 높다. 즉 구현의 난이도가 높다.
참조 :
https://jangjjolkit.tistory.com/61
https://jangjjolkit.tistory.com/62
https://jaeseo0519.tistory.com/314