개발 회고/TIL

[2/14]TIL - 일정 앱 Develop 프로젝트 트러블슈팅 2

mabubsoragodong 2025. 2. 15. 00:29

저번에 썼던 것을 이어서 써보겠다.

 

1. @ManyToOne? @OneToMany? fetch?

먼저 데이터베이스의 컬럼에 리스트가 들어가는 일은 일어나지 않는다.

그래서 @OneToMany를 쓰면 필드가

private List<Comment> comments = new ArrayList<>();

와 같이 되는데 앞서 말했듯 데이터베이스 컬럼에 리스트가 들어갈 수가 없다. 

애초에 Entity 클래스는 데이터베이스의 명세서와 같아서 필드가 컬럼인데 필드에 리스트가 들어간다는 건 컬럼에 리스트 형태가 들어간다는 말이다. 

그래서 일어날 수 가 없는 일이지만 JPA가 해주고 있는 기능이다. 

그래서 @OneToMany@ManyToOne(fetch = FetchType.LAZY)이 없으면 존재할 수 없다. (이건 필수 규칙이므로) → ManyToOne부터 일단 넣고 생각하기!

 

그럼 fetchType은 뭔가??

먼저 즉시로딩과 지연로딩의 개념에 대해서 알아보자. 

이렇게 비유를 들면 쉬울 것 같다. 

 

즉시로딩은 식당에서 비빔밥을 주문했는데 비빔밥, 국, 반찬 모두가 한꺼번에 가져다 주는 것이다. 나는 반찬이랑 국을 먹을 수도 있고 먹지 않을 수도 있는데, 시키지도 않았는데, 가져다 주는 것이다. -> 데이터베이스 세계에서는 JOIN을 이용해서 연관된 엔티티를 모두 즉시로딩한다는 의미이다. 

 

반면 지연로딩은 비빔밥만 주문하면 처음에는 비빔밥만 제공된다. 그리고 김치가 먹고 싶어서, 필요해서 김치를 주문하면 그 때 김치를 가져다 주는 것이다.  -> 데이터베이스 세계에서는 연관된 엔티티를 바로 가져오지 않고 필요할 때 쿼리를 실행해서 가져온다는 의미이다. 

 

즉시로딩은 필요하지도 않은 데이터를 즉시 한꺼번에 가져오는 것이라 메모리 낭비 문제와 비효율적인 쿼리가 실행될 수 있다. 반면 지연로딩은 필요한 데이터만 처음에 가져오기 때문에 속도가 빠르고 메모리 절약도 가능한 것이다. 

 

따라서 다대일, @ManyToOne의 관계에서는 fetchType을 LAZY로 설정해서 메모리를 절약하자. 

그냥 @ManyToOne이면 아묻따 LAZY로 설정하자.

 

@OneToMany를 쓸때에는 연관관계의 주인을 잘 설정해주어야 한다. 

@OneToMany(mappedBy = "team"): mappedBy 사용 시 외래 키 관리 X (연관 관계의 주인이 아님)

그럼 상대방이 @ManyToOne을 하고 외래키를 관리한다.

@ManyToOne(fetch = FetchType.LAZY): ManyToOne 관계에서 외래 키 관리 (@JoinColumn(name = "team_id") // 외래 키 설정)

 

그런데 헷갈리면 그냥 

@OneToMany(mappedBy = “나자신”)으로 생각하자. 

 

  • @ManyToOne에게 무조건 해야되는거
    • @ManyToOne(fetch = FetchType.LAZY)
    • @JoinColumn(name = “상대방_id”)
  • @OneToMany 무조건 해야되는거
    • @OneToMany(mappedBy = “나자신”)

 

 

2. ApplicationException ?

@Getter
public class ApplicationException extends RuntimeException {
    private final HttpStatus status;

    public ApplicationException(String message, HttpStatus status) {
        super(message); // 부모 클래스 (RuntimeException) 의 생성자 호출
        this.status = status; // 예외와 함께 HTTP 상태 코드 저장
    }
}

 

기존 내가 알고있는 예외처리 방식은 그냥 예외가 일어날 것 같은 로직에서 throw를 해주고 catch를 이용해서 예외를 처리해주는 방식이었다. 하지만 이번 프로젝트에서 예외 처리는 exception 패키지를 따로 만들고 예외처리를 통일해서 해주었다. 

 

이 ApplicationException이 하는 것이 무엇인가?

 

  • RuntimeException을 상속받아 예외 클래스의 부모 역할을 함.
  • 예외가 발생하면 예외 메시지(message)와 HTTP 상태 코드(status)를 저장.
  • @Getter를 사용하여 status 값을 외부에서 가져올 수 있도록 함.

그럼 왜 만들었을까?

Spring Boot의 기본 예외 클래스만 사용하면 예외별로 일일이 HttpStatus를 지정해야 한다.
이 클래스를 만들어서 모든 커스텀 예외들이 일관된 구조를 가지도록 설계한 것이다.

 

그럼 커스텀한 예외를 살펴보자. 

import org.springframework.http.HttpStatus;

public class InvalidCredentialException extends ApplicationException {

    public InvalidCredentialException(String message) {
        super(message, HttpStatus.UNAUTHORIZED);
    }
}

 

 

인가가 필요한 api에 접근할 때 인증받지 못한 사용자라면 예외를 던져주어야 할 것이다. 이때 이 예외클래스를 사용한다.

이 때 ApplicationException를 상속받아서 message와 함께 HTTP 상태 코드로 401 Unauthorized (HttpStatus.UNAUTHORIZED)를 설정한다. 

 

그럼 실 사용예시를 살펴보자.

@Transactional(readOnly = true)
    public Long handleLogin(LoginRequestDto dto) {
        User user = userRepository.findByEmail(dto.getEmail()).orElseThrow(
                () -> new InvalidCredentialException("해당 이메일이 존재하지 않습니다."));

        if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
            throw new InvalidCredentialException("비밀번호가 일치하지 않습니다.");
        }
        return user.getId();
    }

이건 서비스에 있는 로그인 함수다. Email로 조회하여 디비에 존재하지 않으면 InvalidCredentialException을 발생시킨다. 해당 예외는 인자로 넣어준 message값이 message로 출력되고 상태코드는 InvalidCredentialException에서 정의했던 401  UNAUTHORIZED 상태코드를 반환한다.

 

결론적으로 인증받지 못한 것에 대한 예외처리를 해주어야 할때 해당 예외발생을 통일 시키기 위해서 커스텀 예외클래스를 만드는데,

그 커스텀 예외들도 일관된 구조를 가지게 하기 위해서 ApplicationException을 만든다.

 

 

3. GlobalExceptionHandler?

그렇다면 이 클래스는 무얼하는 것일까. 이름을 보아하니 전역에서 발생하는 예외들을 다루는 클래스인 것 같다.

 

코드를 살펴보자.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(InvalidCredentialException.class)
    public ResponseEntity<Map<String, Object>> handleInvalidCredentialException(InvalidCredentialException ex) {
        return getErrorResponse(HttpStatus.UNAUTHORIZED, ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException ex) {
        String firstErrorMessage = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .findFirst()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .orElseThrow(() -> new IllegalStateException("검증 에러가 반드시 존재해야 합니다."));

        return getErrorResponse(HttpStatus.BAD_REQUEST, firstErrorMessage);
    }

    private ResponseEntity<Map<String, Object>> getErrorResponse(HttpStatus status, String message) {
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("status", status.name());
        errorResponse.put("code", status.value());
        errorResponse.put("message", message);

        return new ResponseEntity<>(errorResponse, status);
    }
}

 

제일 먼저 보이는 @ControllerAdvice. 뭐하는데 쓰이는 걸까. 이 어노테이션은

 

  • 컨트롤러에서 발생하는 예외를 잡아서 처리하는 클래스이다.
  • @ControllerAdvice를 사용하면 모든 컨트롤러에서 발생하는 예외를 한 곳에서 관리할 수 있다.

그렇다. 이 어노테이션으로 이제 컨트롤러 단에서 발생하는 모든 예외를 한꺼번에 관리할 수 있다.

 

@ExceptionHandler(InvalidCredentialException.class) ?

이 어노테이션은 'InvalidCredentialException(로그인 실패 시 발생하는 예외)이 발생하면' 이 메서드가 실행되겠다는 의미이다.

 

그렇다면 @ExceptionHandler(MethodArgumentNotValidException.class) 요 어노테이션은 입력값 검증 실패(예: @NotBlank, @Size 등의 검증 어노테이션이 실패했을 때) 발생하는 MethodArgumentNotValidException이 발생하면 해당 메서드가 실행되겠다는 의미다. 

 

getErrorResponse() <- 요 함수는 에러 응답 방식을 통일 할 때 쓰겠다는 함수이다.

 

 

이로써 이번 프로젝트를 통해서 내가 모르고 있었던 부분들을 정리해보았다. 

난 사실 검증을 하는 법도, put과 patch의 차이도, 로그인 로직 이해도, 세션 활용도, 트랜잭션도, 페이지네이션도, 연관관계도, 예외처리도 제대로 모르고 그냥 막 갖다 썼던거다. 아무리 걍 막해보는 쌍놈식 공부법을 한다 할 지라도... 뭘 알고 써야지... 이렇게 이틀이면 정리할 걸 그동안 미뤘다.

사실 이걸로 부족하다. 내가 생각하기에 아직 부족한 개념은 영속성 컨텍스트, JPA 사용(이번에 처음으로 existsByEmail과 같은 메서드도 할 수 있는 건지 알게 되었다..)같다. 

스탠다드 세션 수업을 다시 쭉 훑어보아야 할 것 같다. 그리고 챌린지반 세션도 둘러보았는데 내가 전혀 모르고 있는 부분이 몇 있다. 해당 내용도 알아봐야겠다. 

이제 앞으로 일주일간 휴가인데 그 동안 배운 거 까먹지 않게 일상생활에서도 스프링 생각하면서 살아야겠다 ^^

 

 

한국 돌아와서는 

- 일정앱 develop 프로젝트 코드 리팩토링

- 일주일동안 팀원들이 한 코드 보면서 뭐뭐 구현했는지 이해하고 나도 빠르게 구현해보기

 

이제 알았으면 직접 코드로 쳐서 내걸로 만들어봐야한다. 다음 TIL은 직접 친 코드 깃헙 리포를 올릴 수 있도록!