728x90

개요

스터디 중에 AOP에 대한 압박 질문을 받았는데 하나도 대답을 못해서 정리할려고 한다...ㅎㅅㅎ

 


 

1. 사용 목적

애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있다. 

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능
  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다.(로그 추적 기능, 트랜잭션 기능 등...)

보통 기존 프로젝트에 부가 기능을 추가하게 되면 하나의 클래스가 아닌 여러 클래스에 부가 기능을 추가하게 된다.

 

예를 들어서 프로젝트의 모든 클래스에 로그 기능을 추가한다면 하나의 부가 기능(로그 추적)을 여러 곳에 동일하게 사용하게 된다.

 

이러한 부가 기능횡단 관심사라고 한다.

 

1.1 단점

하지만 이러한 기존 프로젝트에 부가 기능을 추가하게 된다면 여러 문제점이 있다. 만약 부가 기능을 적용해야 할 클래스가 100개 라면 100개에 모두 똑같은  부가 기능 코드를 추가해야 하고 거기에 더해 단순 호출이 아닌 try- catch-finally 구문이 필요하다면 더욱 복잡해질 것이다. 

이렇게 많은 클래스에 부가 기능을 힘들게 추가했다고 가정한 후 만약 수정이 필요하다면,,,? 또 100개의 클래스를 하나씩 뜯어 고쳐야 한다는 노가다 과정을 겪어야 한다.

 

 

2. AOP란?

이러한 부가 기능을 적용할 때 위와 같은 문제점이 발생한다. 그래서 많은 개발자들은 오랜 시간 고민해왔다. 고민 끝에 AOP라는 용어가 나오게 되었다.

AOP는 관점 지향 프로그래밍의 약어로 부가 기능을 핵심 기능에서 분리해 한 곳으로 관심하도로 하고 이 부가 기능을 어디에 적용할지 선택하는 기능을 합한 하나의 모듈이다. 

 

 

 

3. AOP 용어 정리

AOP 프록시

AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.

 

조인 포인트 ( Join Point )

어드바이스가 적용될 수 있는 위치로, AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.

스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.

 

포인트컷 ( Pointcut )

조인 포인트 중에서 어드바이스(부가 기능)를 어디에 적용할 지, 적용하지 않을 지 위치를 판단하는 필터링하는 기능 ( 주로 AspectJ 표현식을 사용해서 지정 )

프록시를 사용하는 스프링 AOP는 메서드 실행 지점을 포인트 컷으로 필터링 한다.

 

타겟 ( Target )

어드바이스를 받는 객체, 포인트컷으로 결정

 

어드바이스 ( Advice )

부가 기능

특정 조인 포인트에서 Aspect에 의해 취해지는 조치

Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음

 

애스펙트( Aspect )

어드바이스 + 포인트컷을 모듈화 한 것

하나의 어드바이스만이 아닌 여러 어드바이스와 포인트 컷이 함께 존재할 수 있다.

 

어드바이저 ( Advisor )

하나의 어드바이스와 하나의 포인트 컷으로 구성

즉, 어드바이스 + 포인트 컷 = 어드바이저

 

 

 

4. AOP 적용 방식

AOP의 적용 방식은 크게 3가지가 있다.

  • 컴파일 시점
  • 클래스 로딩 시점
  • 런타임 시점 ( 프록시 사용 )

컴파일 시점과 클래스 로딩 시점 적용 방식은 AspectJ 프레임워크를 직접 사용해야 하고, 이 AspectJ를 학습하기 위해선 엄청난 분량과 설정의 번거로움이 있다. 그래서 주로 런타임 시점 적용 방식을 사용하는 스프링 AOP를 사용 한다.

 

 

 

 

5. AOP 적용 가능 위치

주로 사용하는 런타임 시점 적용 방식의 스프링 AOP 관점에서의 방식이다. 

  • 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
  • 프록시 방식을 사용하는 스프링 AOP는 스프링 컨테이너에 해당 @Aspect을 빈 등록을 해야 AOP를 적용할 수 있다.

 

 

6. 스프링에서 AOP 적용하기 (@Aspect)

스프링 AOP를 적용하기 위해서는 아래의 라이브러리를 build.gradle에 의존성을 추가 해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-aop' // 스프링 aop 추가

위의 라이브러리를 추가하면 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록해준다.

이러한 스프링 부트의 자동 설정은 "AnnotationAwareAspectJAutoProxyCreator"라는 빈 후처리기가 스프링 빈에 자동으로 등록해주는데, 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.

 

6.1 AnnotationAwareAspectJAutoProxyCreator란? 

1. @Aspect 어노테이션이 붙은 클래스를 Advisor(어드바이저)로 변환하여 저장

2. Advisor(어드바이저)를 자동으로 찾아와 프록시를 생성하고 Pointcut(프록시 적용 대상 필터링)을 보고 프록시가 필요한 곳에 Advice(부가 기능)을 적용

 

 

6.2 코드 - @Around 

  • 스프링은 @Aspect 애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.
  • 어드바이저로 사용할 클래스에 @Aspect 어노테이션을 붙여줌으로써 스프링 AOP를 적용할 수 있다.
@Aspect
public class LogTraceAspect {
 
    @Around("execution(* hello.proxy.app..*(..))")  // 포인트컷 (AspectJ 표현식)
    public Object execute(ProceedingJoinPoint joinPoint) { // 어드바이스
        // 어드바이스 로직
    }
}

@Around 어드바이스를 사용할 경우 메서드의 파리미터로 "ProceedingJoinPoint"를 꼭 넣어줘야 한다.

ProceedingJoinPoint의 proceed()는 다음 어드바이스나 타켓을 호출하는 것으로, 어드바이스를 사용하기 위해서는 꼭 proceed() 메서드를 호출해줘야 한다.

 

이 외에도  호출되는 대상 객체에 대한 정보, 실행되는 메서드에 대한 정보 등이 필요할 때가 있는데 이 경우에는

ProceedingJoinPoint 인터페이스가 제공하는 아래의 메서드를 사용할 수 있다. (이 외에도 더 있지만 일부만 설명)

메서드 설 명
Signature getSignature()  호출되는 메서드에 대한 정보를 반환
Object getTarget()  대상 객체를 반환
String getName  메서드의 이름을 반환
String toLongString()  메서드를 완전하게 표현한 문장을 반환 (메서드의 리턴 타입, 파라미터 타입 모두 표시)

 

이렇게  ProceedingJoinPoint 호출되는 객체에 대한 정보나, 실행되는 메서드의 정보 알 수 있는 이유

스프링 부트 자동 설정으로 "AnnotationAwareAspectJAutoProxyCreator" 이라는 자동 프록시 생성기가 빈 등록되어 있는데,

이 자동 프록시 생성기가 @Aspect가 붙은 클래스를 보고 Advisor(어드바이저)로 변환해 저장해준다.

그리고 이 Advisor(어드바이저)를 보고 포인트컷의 대상이 되는 것들을 "ProxyFactory"에 인자로 넘겨 자동으로 프록시를 생성하고 적용해준다.

여기서 생성된 프록시 객체가 메서드를 호출할 때, ProceedingJoinPoint 객체를 생성하고 이를 advice에 전달한다.

즉, ProceedingJoinPoint는 프록시가 메서드를 호출하는 시점의 정보를 가져 어드바이스가 적용되는 대상을 이미 알고 있다.

 

 

6.3 @Pointcut

@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리해 재사용할 수 도 있다.

@Slf4j
@Aspect
@Component
public class AspectExample {
 
    // hello.aop.test 패키지와 하위 패키지에 적용
    @Pointcut("execution(* hello.aop.test..*(..))")
    private void allTestLog() {} //  포인트컷 시그니쳐 
 
    @Around("allTestLog()") // 포인트컷을 메서드로 만들어 사용
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 실제 타깃 호출
    }
}

 

 

 

7. 어드바이스 종류

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능 (사실 이거 하나만 사용해도 무방)
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

@Around를 제외한 나머지 어드바이스들은 JoinPoint를 첫 번째 파라미터에 사용한다. (생략도 가능)

( JoinPoint는 ProceedingJoinPoint의 부모 타입)

 

728x90
728x90

스프링 배치를 사용하여 시간 기반 트리거로 특정 조건(읽지 않은 알림이 일주일 지난 경우 다시 알림 발송)을 구현했을 때 다중 인스턴스 환경에서는 인스턴스마다 배치 작업이 중복 실행되는 문제가 발생할 수 있다. 그 외로 스프링 배치 관련해서 어떤 질문이 나올지 정리할려고 한다.

 

 

 

 

예상 질문 1: "인스턴스가 두 대 이상일 때, 알림이 중복 발송되는 문제를 어떻게 해결할 수 있나요?"

 

답변:

"스프링 배치에서 다중 인스턴스 환경에서 중복 작업이 발생하지 않도록 하기 위해서는 배치 작업의 동시성 제어가 필요합니다. 이 문제를 해결하기 위해 다음과 같은 방법을 사용할 수 있습니다.

  1. Spring Batch의 JobInstance 및 JobExecution을 활용한 중복 방지: 스프링 배치는 기본적으로 JobInstance라는 개념을 사용하여 동일한 파라미터로 실행된 배치 작업이 여러 번 실행되지 않도록 방지합니다. 이를 통해 동일한 작업이 두 인스턴스에서 동시에 실행되지 않도록 제어할 수 있습니다. 특정 배치 작업에 고유한 ID나 파라미터를 부여하여 같은 작업이 중복 실행되지 않도록 처리할 수 있습니다.
  2. 쿼츠 스케줄러(Quartz Scheduler)를 사용한 클러스터 환경 지원: 클러스터링된 환경에서 배치 작업이 중복 실행되지 않도록 Quartz Scheduler를 사용할 수 있습니다. 쿼츠 스케줄러는 분산된 여러 인스턴스에서 하나의 작업만 실행되도록 스케줄링을 관리할 수 있는 기능을 제공합니다. 이를 통해 각 인스턴스가 스케줄링된 작업의 소유권을 체크하고, 작업을 중복 실행하지 않도록 보장할 수 있습니다.
  3. DB를 활용한 분산 락(Distributed Lock): 배치 작업이 실행되기 전에 데이터베이스를 활용하여 분산 락(Distributed Lock)을 구현할 수 있습니다. 락을 획득한 인스턴스만 배치 작업을 실행하게 하고, 나머지 인스턴스는 작업을 대기하거나 실행하지 않도록 처리할 수 있습니다. Redis와 같은 캐시 서버의 락 기능을 활용해 배치 트리거를 하나의 인스턴스에서만 실행할 수 있도록 구현할 수 있습니다."

=> 분산락이 제일 많이 사용되는 방

 

예상 질문 2: "분산 락을 사용하는 방법은 무엇인가요?"

 

답변:

 

분산 락(Distributed Lock)은 여러 대의 서버나 인스턴스가 동시에 동일한 자원에 접근할 때 하나의 인스턴스만 자원에 접근할 수 있도록 제어하는 메커니즘이다. 분산 시스템에서 여러 서버나 인스턴스가 동일한 작업을 중복해서 수행하지 않도록 하는 것이 목적이다.

서버 A와 서버 B가 동시에 동일한 데이터베이스 테이블을 갱신하려고 하면 충돌이나 데이터 불일치가 발생할 수 있습니다. 이때 분산 락을 통해 한 번에 하나의 서버만 해당 테이블을 갱신할 수 있도록 제어하는 것이다.

 

분산 락의 주요 개념

  1. 락 획득: 특정 자원을 사용하기 전에 인스턴스가 락을 먼저 획득해야 한다. 락을 획득한 인스턴스는 해당 자원에 대해 독점적으로 작업할 수 있다.
  2. 락 해제: 작업이 완료되면 락을 해제하여 다른 인스턴스가 자원에 접근할 수 있도록 한다.
  3. TTL(Time To Live): 락이 너무 오래 걸리거나, 시스템 오류로 인해 락이 해제되지 않을 경우를 대비해 일정 시간이 지나면 자동으로 락을 해제하는 방식이다.

Redis를 사용한 분산 락 예시

  1. setnx 명령: Redis의 setnx(Set if Not Exists) 명령은 주어진 키가 존재하지 않을 때만 값을 설정하는 명령이다. 이를 통해 하나의 인스턴스만 특정 키에 락을 설정할 수 있게 된다.
  2. TTL 설정: 만약 배치 작업이 실패하거나 예기치 못한 오류가 발생할 경우 락이 계속 유지되는 것을 방지하기 위해 TTL을 설정하여 자동으로 락이 해제되도록 한다.
  3. 락 해제: 작업이 완료되면 해당 키를 삭제하여 락을 해제한다.

분산 락이 필요한 이유

분산 환경에서 여러 인스턴스가 동일한 자원에 동시에 접근하게 되면 데이터 일관성 문제가 발생하거나 작업이 중복으로 실행될 수 있다. 분산 락은 이를 방지하고 자원을 안전하게 제어하여 시스템의 안정성을 높이는 데 중요하다.

이러한 락을 잘못 관리하면 데드락(Deadlock) 같은 문제가 생길 수 있으므로 락의 적절한 관리가 필요하다.


"분산 락을 사용하여 여러 인스턴스에서 배치 작업이 중복되지 않게 제어하는 방법은 보통 데이터베이스Redis와 같은 캐시 시스템을 활용하는 방식입니다. 그 중 하나인 Redis를 사용한 분산 락 예를 들면:

배치 작업을 실행할 때 Redis의 setnx 명령을 사용하여 락을 생성합니다. 이 락은 하나의 인스턴스만 획득할 수 있으며, 다른 인스턴스들은 이 락을 획득하지 못하게 됩니다.

락을 획득한 인스턴스가 배치 작업을 실행하고, 작업이 끝나면 락을 해제합니다.

TTL(Time To Live)을 설정하여, 만약 작업이 예상보다 오래 걸리거나 예기치 못한 상황이 발생하더라도 락이 자동으로 해제되도록 할 수 있습니다.

이 방법을 사용하면 하나의 인스턴스만 작업을 실행하므로, 알림이 중복 발송되는 문제를 방지할 수 있습니다."

 

 

예상 질문 3: "스프링 배치에서 트랜잭션 관리를 어떻게 처리했나요?"

 

답변:

"스프링 배치는 기본적으로 트랜잭션 관리를 지원하며, 각 단계(Step)마다 트랜잭션 경계를 설정할 수 있습니다. 알림 발송과 같은 중요한 작업에서는 데이터의 일관성을 보장하기 위해 트랜잭션을 적용하여 작업이 성공적으로 완료되지 않으면 롤백하도록 설정했습니다. 예를 들어, 읽지 않은 알림을 조회하고 다시 알림을 보내는 과정에서 문제가 발생할 경우, 해당 작업은 롤백되어 알림이 중복되거나 잘못된 상태로 저장되지 않도록 했습니다.

또한, 알림을 발송한 후 성공적으로 처리된 데이터에 대해서만 커밋이 이루어지도록 하여 데이터 일관성을 유지했습니다."

 

 

예상 질문 4: "만약 한 인스턴스에서 배치 작업 중 오류가 발생하면 어떻게 처리할 수 있나요?"

 

답변:

"배치 작업 중 오류가 발생할 수 있는 다양한 상황에 대비해 스프링 배치의 재시도 및 실패 처리 메커니즘을 적용했습니다. RetryTemplate을 사용하여 특정 단계에서 오류가 발생했을 때, 자동으로 재시도하도록 구성하였습니다. 오류가 발생해도 일정 횟수 동안 재시도할 수 있는 구조를 쉽게 구현할 수 있습니다. 또한, SkipPolicy를 설정하여 일정 횟수 이상의 오류가 발생한 경우 해당 배치 작업을 중단하고, 오류를 로깅하거나 별도의 실패 처리 로직을 실행할 수 있습니다.

이외에도, 실패한 작업을 별도의 에러 큐에 넣어 나중에 다시 처리할 수 있도록 설정할 수 있습니다. 이렇게 하면 시스템 장애나 예기치 않은 오류 발생 시에도 작업이 중단되지 않고, 안정적으로 복구할 수 있도록 대비할 수 있습니다."

 

기본적으로 RetryTemplate을 사용해서 배치 오류 해결을 위해 재전송을 사용하는데 트랜잭션 관리로도 가능하다.

배치 작업의 각 스텝에 대해 트랜잭션을 설정하여 작업 중 오류가 발생하면 트랜잭션을 롤백하고 작업을 재시도하지 않도록 할 수 있다. 트랜잭션을 사용하면 배치 작업에서 일부 데이터가 처리된 후 오류가 발생해도 처리된 데이터가 커밋되지 않게 하여 데이터 일관성을 유지할 수 있다.

728x90
728x90

개요

스프링 배치를 사용해서 알림 재전송 기능을 구현했을 때 다양한 방식들의 트리거링 방식에 대해서 정리할려고 한다.

 

 

배치 트리거링(Batch Triggering)

 

  • 배치 트리거링(Batch Triggering)은 특정 조건이나 이벤트가 발생할 때 자동으로 배치 작업을 시작하는 메커니즘을 말한다.
  • 배치 작업은 대량의 데이터를 처리하거나 주기적으로 실행되는 작업을 자동화하는 데 사용되는데 이때 트리거링을 통해 작업 시작을 제어한다.

 

 

배치 트리거링 방식

  • 시간 기반 트리거(Time-based Trigger)
    • 특정 시간이나 주기로 배치 작업이 실행되도록 설정하는 방식이다.
    • 매일 자정에 데이터 백업 작업을 실행하는 경우
  • 이벤트 기반 트리거(Event-based Trigger)
    • 특정 이벤트가 발생할 때 배치 작업을 실행하는 방식이다.
    • 사용자가 주문을 완료하면 해당 주문 정보를 처리하는 배치 작업이 실행되는 경우
  • 조건 기반 트리거(Condition-based Trigger)
    • 특정 조건이 충족될 때 배치 작업을 실행하는 방식이다.
    • 데이터베이스에 일정한 양의 데이터가 쌓이면 이를 처리하는 배치 작업이 실행되는 경우

 

배치 트리거링은 시스템 효율성을 높이고 사용자가 수동으로 작업을 실행하지 않아도 자동으로 필요한 처리가 이뤄지도록 돕는 중요한 개념이다.!!!!

728x90
728x90

개요

1편에서 배치에 대해 살펴보았고 이제 내가 하는 프로젝트에 배치를 적용해볼 예정이다. 

 

일단 나는 SSE 알림 기능을 구현했는데, 일정 기간동안 읽지 않은 알림이 있다면 사용자에게 "읽지 않은 알림이 있습니다." 라는 문구를 띄어줄려고 한다. 주기적인 스케줄을 가지고 필요한 알림을 처리하기 위한 기술이 필요했다. 그래서 주기적인 schedule 에 따라 Spring Batch를 통해서 필요 데이터를 reading, 알람을 보내는 기능을 구현하기로 했고 알람은 SSE를 사용하기로 했다.

 

사용자가 접근하지 않는 시간대인 매일 자정에 매번 실행해서 7일동안 읽지않은 알림이 존재할 경우 알림을 보내는 역할을 할 것이다.

 

 


설정

 

build.gradle

// spring batch
implementation 'org.springframework.boot:spring-boot-starter-batch'

batch 의존성 추가

 

 

application.yml

spring:
  application:
    name: {name}

  batch:
    jdbc:
      initialize-schema: always #batch 스키마 자동 생성
    job:
      enabled: false #시작과 동시에 실해되는건 방지

 

 

 

Cofing.java

@Configuration
@EnableBatchProcessing
@Slf4j
@RequiredArgsConstructor
public class NotificationBatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager platformTransactionManager;
    private final NotificationRepository notificationRepository;

    // 배치 작업의 전체 실행
    @Bean
    public Job deleteNotificationsJob(Step deleteUnreadNotificationsStep) {
        log.info(">>> Creating deleteNotificationsJob");
        return new JobBuilder("deleteNotificationsJob", jobRepository)
                .start(deleteUnreadNotificationsStep)
                .build();
    }

    // 배치 작업 내 실행되는 개별 처리
    @Bean
    public Step deleteUnreadNotificationsStep() {
        log.info(">>> Creating deleteUnreadNotificationsStep");
        return new StepBuilder("deleteUnreadNotificationsStep", jobRepository)
                .tasklet(deleteUnreadNotificationsTasklet(), platformTransactionManager)
                .build();
    }

    // 10분 이상 읽지 않은 알림 삭제
    @Bean
    public Tasklet deleteUnreadNotificationsTasklet() {
        return (contribution, chunkContext) -> {
            LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(10);
            List<Notification> notificationsToDelete = notificationRepository.findByIsReadAndCreatedAtBefore(false, cutoffTime);
            notificationRepository.deleteAll(notificationsToDelete);
            log.info("Deleted unread notifications older than 10 minutes");
            return RepeatStatus.FINISHED;
        };
    }
}

 

 

Tasklet

  • 일반적인 Component와 비슷한 개념, 개발자가 지정한 커스텀한 기능을 위한 단위
  • 개발자가 이 STEP에서 하고 싶은 내용을 자유롭게 만들 수 있다.

 

 

BatchScheduler.java

@Component
@Slf4j
@RequiredArgsConstructor
public class NotificationBatchScheduler {

    private final JobLauncher jobLauncher; // 정의된 job 을 주기적 실행
    private final Job deleteNotificationsJob;

    @Scheduled(cron="0 0 12 * * *", zone = "Asia/Seoul") // 매일 자정에 실행
    public void runDeleteUnreadNotificationsJob() {
        try {
            jobLauncher.run(deleteNotificationsJob, new JobParametersBuilder()
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters());
            log.info("Notification deletion job executed successfully.");
        } catch (Exception e) {
            log.error("Failed to execute notification deletion job", e);
        }
    }
}

 

jobLauncher.run()메서드는 첫번째 파라미터로 Job, 두번째 파라미터로 Job Parameter를 받고있다.
Job Parameter의 역할은 반복해서 실행되는 Job의 유일한 ID이다.

Launcher 가 어떤 배치 작업(Job)을 실행할 지에 대해 작성한다. 

Job은 어떤 데이터들을 통해서 어떤 작업을 실행할 지, 실행 순서 등에 대한 내용(Step)을 담고 있어야 한다.

 

 

Scheduler

  • batch를 수행하기 위한 Scheduler를 파일에 구성(매일 자정에 실행)
Spring Batch는 Batch Job을 관리하지만 `Job`을 구동하거나 실행시키는 기능은 지원하고 있지않다.
Spring에서 Batch Job을 실행시키기 위해서는 `Quartz, Scheduler`와 같은 전용 Scheduler를 사용해야한다.
728x90
728x90

 

 

 

배치란? 스프링 배치란? 

  • 배치 는 일정 시간마다 또는 특정 조건에 따라 대규모 데이터를 일괄 처리하는 방식을 의미한다.
  • 스프링 배치(Spring Batch)는 이러한 배치 작업을 보다 쉽게 구현하고 관리할 수 있도록 돕는 프레임워크이다.
  • 스프링 배치는 스프링 생태계의 일환으로, 스프링의 강력한 기능을 활용하여 배치 작업을 안정적이고 효율적으로 수행할 수 있도록 지원한다.
  • 이를 통해 개발자는 복잡한 배치 처리 로직을 간결하게 구현하고 모니터링 기능을 통해 배치 작업을 효과적으로 관리할 수 있다.

 

스프링 배치와 스프링 스케줄러 차이

  • 많은 개발자들이 이 두 가지를 혼동하곤 하지만, 스프링 배치(Spring Batch)와 스프링 스케줄러(Spring Scheduler)는 서로 다른 목적과 기능을 가지고 있다.

 

스프링 배치

  • 대량의 데이터를 효율적으로 처리하기 위해 설계된 프레임워크
  • 복잡한 배치 작업을 구성하고 실행하기 위한 다양한 도구와 기능을 제공
    • 배치 프로세스를 주기적으로 커밋: 막강한 커밋 전략을 제공
    • 동시 다발적인 job 의 배치 처리, 대용량 병렬 처리
    • 실패 후 수동 또는 스케줄링에 의한 재시작: 오류에 대한 대처 옵션을 제공
    • 반복, 재시도, skip 처리 : 실패한 배치 작업을 중단된 지점부터 다시 시작하거나 skip 할 수 있다.
    • 모니터링: 배치 작업의 진행 상태와 소요 시간 등의 정보를 제공
    • 트랜잭션 관리: 트랜잭션 관리를 지원하여 데이터의 일관성 유지

 

스프링 배치는 주로 금융, 전자상거래, 건강관리 등 대량의 데이터 처리가 필요한 도메인에서 널리 사용된다.

배치(batch)는 데이터를 실시간으로 처리하는게 아니라, 일괄적으로 모아서 한번에 처리하는 작업을 의미한다.

 

 

스프링 스케줄러

  • 스프링 스케줄러는 특정 시간에 작업을 실행하기 위해 설계된 도구이다.
  • 반복적인 작업을 자동화하고, 시간 기반의 작업을 쉽게 설정할 수 있다.
    • 시간 기반 작업 실행: 특정 시간 또는 주기적으로 작업을 실행할 수 있다.
    • 다양한 스케줄링 방식: 크론 표현식(Cron Expression) 또는 Fixed Delay 를 통해 작업 스케줄을 설정할 수 있다.
    • 비동기 처리: 스케줄된 작업을 비동기적으로 실행하여 애플리케이션의 성능을 유지할 수 있다.
    • 유연한 구성: XML 설정 또는 애노테이션을 통해 쉽게 설정할 수 있다.

 

스프링 스케줄러는 주로 정기적인 백업, 이메일 알림, 데이터 동기화 등의 작업을 자동화하는 데 사용된다.

 

 

특징

                                              스프링 배치 (Spring Batch)                                         스프링 스케줄러 (Spring Scheduler)

목적 대량 데이터 일괄 처리 시간 기반 작업 실행
주요 기능 트랜잭션 관리, 재시작 및 중단, 데이터 검증 및 오류 처리 크론 표현식, 고정된 지연, 비동기 처리
사용 사례 금융 데이터 처리, 대량 파일 처리, 시스템 통합 정기 백업, 이메일 알림, 데이터 동기화
구성 방식 주로 XML 설정 또는 Java Config 애노테이션 또는 XML 설정

 

 

 


 

스프링 배치 구조

 

스프링배치 주요 흐름 이미지 출처 (https://terasoluna-batch.github.io/)

 

  1. 스프링 배치 (Spring Batch):
    • 스프링 배치는 대규모 데이터 처리 작업을 자바 기반의 배치 처리 어플리케이션을 만들기 위한 프레임워크
    • 대용량 데이터를 처리하는 경우나 주기적인 업무 일괄 처리 등에 사용
    • 스프링 배치는 배치 작업을 트랜잭션 처리와 함께 관리하고, 실패한 작업을 복구하고 재시작할 수 있는 기능을 제공
    • 잡(Job), 스텝(Step), 리더(Reader), 프로세서(Processor), 라이터(Writer) 등의 개념을 이용하여 배치 처리를 설계하고 실행한다.
    • job은 여러개의 step 으로 구성되고 step은 taskelt(기능) 으로 구성된다.
    • 나는 tasklet으로 구현하였다.!!
  2. 쿼츠 (Quartz):
    • 쿼츠는 자바 기반의 오픈 소스 스케줄링 라이브러리
    • 스케줄링 작업을 관리하고 실행할 수 있는 기능을 제공
    • 주기적으로 실행되어야 하는 작업이나 예약된 작업을 관리하고 실행하는 데 사용
    • 쿼츠는 크론 표현식을 사용하여 실행 스케줄을 설정하고, 다양한 트리거(trigger)를 통해 작업을 실행할 수 있음
    • 분산 환경에서도 사용할 수 있는 확장 가능한 아키텍처를 가지고 있어, 대규모 시스템에서도 유연하게 활용됨
요약하자면, 스프링 배치는 대규모 데이터 처리를 위한 일괄 처리 프레임워크이고, 쿼츠는 예약된 작업을 스케줄링하고 실행하는 데 사용되는 라이브러리 
-> 함께 사용되어 시스템에서 정기적인 작업 처리와 스케줄링을 효과적으로 관리할 수 있음

 

 

 

Job : 전체 배치 프로세스를 캡슐화한 엔티티, 배치 처리 과정을 하나의 단위로 만들어 놓은 객체

 

JobInstance : Job 실행의 단위(job이 각각 실행할 때마다 따로 추적할 수 있도록 매 실행마다 생성되며 특정 job과 식별 가능한 JobParameters에 상응하는 JobInstance는 단 한 개뿐이다.)

 

JobParameters : JobInstance에 전달되는 매개변수 역할, JobInstance를 구분하는 식별자 역할

 

JobExecution : Job에 대한 실행 시도 정보를 담은 객체.

 

Step : 배치 Job의 배치처리를 정의하고 순차적인 단계를 캡슐화 한 도메인 객체. Job은 하나 이상의 Step을 가져야 한다.

 

StepExecution : Step에 대한 실행 시도 정보를 담은 객체.

 

ExecutionContext : 프레임워크에서 유지/관리하는 key-value의 컬렉션. StepExecution객체 또는 JobExecution객체에 속하는 상태를 저장

 

JobExecutionContext : Commit 시점에 저장

 

StepExecutionContext : 실행 사이에 저장

 

JobRepository : 위에 업근된 모든 저장(Persistence)매커니즘을 담당.(JobLauncher, Job, Step 구현체에 CRUD 기능을 제공)

 

JobLauncher : 주어진 JobParameters로 Job을 실행하는 간단한 인터페이스.

 

- Step에서 아이템을 한 번에 하나씩 읽어오는 작업을 추상화한 개념

Tasklet 

   or

Item Reader 

Item Writer : Step에서 배치나 청크 단위로 아이템을 출력하는 작업을 추상화한다.

Item Processor : 아이템을 처리하는 비즈니스 로직을 나타내는 추상화 개념

 


 

https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide

 

Spring Batch 5.0 Migration Guide

Spring Batch is a framework for writing batch applications using Java and Spring - spring-projects/spring-batch

github.com

스프링배치5부터는 매인 애플리케이션에 @EnableBatchProcessing 어노테이션을 사용할 필요가 없다.

자세한 내용은 마이그레이션 가이드부터 읽어 보길 권장한다!!

 

 

728x90
728x90

개요

이전 블로그에 지연로딩, 즉시로딩에 대해서 살펴보았다.

JPA를 사용하다 보면 바로 N+1의 문제에 마주치고 바로 Fetch Join을 접하게 된다.
처음 Fetch Join을 접했을 때 왜 일반 Join으로 해결하면 안되는지에 대해 명확히 정리가 안된 채로 Fetch Join을 사용했다.
어떤 문제 때문에 일반 join으로 N+1을 해결하지 못하는지를 시작으로 해서 Fetch Join, Join의 차이점을 정리해보겠다.

 

 

Join, Fetch Join 차이점 요약

  • 일반 Join
    • Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화
    • 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 데이터는 필요하지 않지만 연관 Entity가 검색조건에는 필요한 경우에 주로 사용됨

 

  • Fetch Join
    • 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화
    • Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 Lazy인 Entity를 참조하더라도 이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1문제가 해결됨

 

 

N+1 문제

단순 테이블 조회 쿼리를 하나 생성하였는데, 연관관계에 있는 데이터를 모두 불러오기 위해서 select 쿼리가 테이블의 레코드 수(혹은 호출한 데이터 수) 만큼 더 생성되는 문제를 의미한다.

 

즉시로딩은 최초 테이블 데이터를 모두 불러온 후 바로 연관관계가 있는 엔티티를 조회하는 방식을,

지연로딩은 최초 테이블 데이터를 모두 불러온 후 필요한 시점에 연관관계가 있는 엔티티를 조회하는 방식을 채택한다.

 

즉시로딩은 워딩 그대로 조회 시점에 연관관계에 있는 엔티티가 모두 조회되어 N+1 문제가 생긴다는 것을 직관적으로 알 수 있지만 지연로딩은 얼핏보면 로딩 시점에 쿼리가 더 나가지 않아 N+1 문제가 발생하지 않을 것으로 보인다. 하지만 지연로딩 N+1 문제를 피해갈 수는 없는데, 그 이유는 연관관계에 있는 엔티티가 조회 시점에 영속화되지 않았기 때문이다.

 

일반적으로 JPA는 JPQL 로 생성된 쿼리문을 데이터베이스와 connection 을 만든 후에 전송하고, 이에 대한 결과 데이터를 전달받아 실제 엔티티 객체를 생성하고 이를 영속화 한다.

 

 

 

예시

@Entity
class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    
    String title;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    List<Comment> cpmments = ArrayList()
}

@Entity
class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    
    String content;

    @ManyToOne(fetch = FetchType.LAZY)
    Post post;
}
@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @Transactional(readOnly = true)
    public void getMockposts() {
        postRepository.findAll().forEach(post -> {
            post.getpPostComments().forEach(cpmments ->
                System.out.println(cpmments.getContent())
            );
        });
    }
}

위 코드에서 N+1 문제가 발생할 수 있다. 예를 들어, 100개의 게시글이 있다고 가정하면, PostRepository의 findAll() 메서드가 호출될 때 모든 게시글을 조회한다. 이때, 각 게시글의 댓글을 조회하기 위해 추가로 100번의 쿼리가 실행다. 이는 총 101번의 쿼리 실행으로 이어지며, 매우 비효율적인 방법이다. 즉, 다음과 같이 SQL이 실행되면서 N+1 문제가 발생한 것을 알 수 있다.

SELECT * FROM post;
SELECT * FROM comment WHERE post_id = 1;
SELECT * FROM comment WHERE post_id = 2;
SELECT * FROM comment WHERE post_id = 3;
...
SELECT * FROM comment WHERE post_id = 99;
SELECT * FROM comment WHERE post_id = 100;

 

 

해결 방법

1. Fetch Join (@Query 어노테이션 사용)

  • Fetch Join은 N+1 문제를 방지하고 성능을 최적화할 수 있는 수단으로서 데이터베이스에서 연관된 엔터티를 함께 로딩하는 방법이다.
  • Fetch Join을 사용하면 조회 성능을 최적화 할 수 있다.
  • 연관된 엔티티를 쿼리 시 함께 조회하는 방법이다.
  • SQL 쿼리에 직접 Join fetch를 명시한다.
  • 조회의 주체가 되는 엔티티 외에 Fetch Join이 걸린 연관 엔티티도 같이 영속화 시켜준다.
  •  
@Repository 
public interface PostRepository extends JpaRepository<Post, String> {
@Query("SELECT p from Post p JOIN FETCH p.comments")
List<Post> findAllPostsWithComments();
}
  •  "JOIN FETCH p.comments" 구문을 통해서 게시글을 조회할 때 댓글 엔티티도 함께 조회한다.
  • join fetch는 Fetch Join을 사용하여 연관된 데이터를 조회하겠다는 뜻이다.

 

2. Entity Graph

  • EntityGraph를 사용하면 Fetch Join를 사용한 경우와 마찬가지로 연관된 엔티티나 컬렉션을 같이 조회 할 수 있다.
  • JPQL과 함께 사용하며 attributePaths에 쿼리 수행 시 조회할 필드를 지정해준다.
  • 지정된 필드는 지연 로딩이 아닌 즉시 로딩으로 조회된다.
  • EntityGraph는 Fetch Join과는 다르게 Outer Join이 사용되어 동작한다
@Repository 
public interface PostRepository extends JpaRepository<Post, String> {

@EntityGraph(
        attributePaths = {"comments"},
        type = EntityGraph.EntityGraphType.LOAD
    )
@Query("SELECT p FROM post p")
List<Post> findAllPostsWithComments();

}

 

 

Fetch Join 및 EntityGraph 사용 시 발생하는 카테시안 곱 문제

Fetch Join 또는 EntityGraph를 사용한 경우엔 카테시안 곱(Cartesian Product)으로 인하여 중복 문제가 발생할 수 있습니다. 카테시안 곱은 연관관계의 엔티티에 Join 규칙을 사용하지 않아 결합 조건이 주어지지 않았을 때 모든 데이터의 경우의 수를 결합(M * N)하여 반환하는 문제입니다. 즉, 조인 조건이 없는 경우에 대한 두 테이블의 결합 결과를 반환해야 하기 때문에 존재하는 모든 행의 개수를 곱한 결과를 반환하게 됩니다. 카테시안 곱 문제를 해결하기 위해서는 다음과 같은 방법을 사용합니다.
 

 

 


1. JPQL에 DISTINCT 사용하기


JPQL을 이용한 쿼리를 사용할 때 다음과 같이 DISTINCT를 사용하여 중복을 제거합니다.

@Repository 
public interface PostRepository extends JpaRepository<Post, String> {

    @Query("SELECT DISTINCT p from Post p JOIN FETCH p.comments")
	List<Post> findAllPostsWithComments();
    
    @EntityGraph(
        attributePaths = {"comments"},
        type = EntityGraph.EntityGraphType.LOAD
    )
	@Query("SELECT DISTINCT p FROM post p")
	List<Post> findAllPostsWithComments();
}


 

2. 연관관계의 필드 타입에 Set 사용하기

  • Set은 중복을 허용하지 않기 때문에 중복 데이터가 들어가지 않지만 순서가 보장되지 않는다.
  • 이러한 경우 다음과 같이 연관관계의 필드에 LinkedHashSet을 사용하여 중복을 제거하고 순서를 보장 해줄 수 있다.
@Getter
@NoArgsConstructor
@Entity(name = "posts")
public class post {

    @OneToMany(mappedBy = "post")
    private Set<Comment> comments = new LinkedHashSet<>();

}

 

 

728x90
728x90

개요 

스프링 JPA를 활용한 프로젝트 리팩토링 과정에서 JPA N+1 문제가 발생하여 정리해볼려고 한다.

 

Fetch 전략

특정 엔티티를 조회할 때, 연관된 다른 엔티티를 [언제] 불러오는지에 대한 옵션이다. 위와 같이 연관관계 매핑 어노테이션의 옵션을 통해 전략을 명시한다. 옵션에는 두 가지의 전략이 있다. 

 

 

1. 즉시로딩

  • 즉시로딩이란 연관된 모든 엔티티를 함께 즉시 로드하는 방식이다.
  • 즉, 부모 엔티티를 조회할 때 연관된 자식 엔티티들도 함께 한번에 가져온다. 
  • 현재 엔티티를 조회한 [직후], 연관된 엔티티까지 조회한다.
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private User user;
}

 

위 Post 엔티티는 User와 다대일(@ManyToOne) 관계를 맺고 있으며, 즉시 로딩(EAGER)으로 설정되었다. 이 경우 Post를 조회할 때 User 정보도 함께 조회된다. 즉, Post 엔티티를 가져오면 바로 User 엔티티도 DB에서 쿼리되어 로드된다.

장점으로는 쿼리가 한 번만 실행되므로 사용 시 추가적인 데이터베이스 접근이 필요 없다. 이 방법으로 인해 N+1 문제를 해결할 수 있지만, 불필요하게 많은 데이터를 한 번에 로드할 수 있어 성능에 영향을 줄 수 있다.  

 

 

 

 

2. 지연로딩

  • 지연로딩이란 연관된 엔티티를 즉시 로드하지 않고 실제로 해당 엔티티에 접근할 때 데이터를 로드하는 방식이다.
  • 즉 부모 엔티티만 먼저 조회하고 자식 엔티티는 실제로 필요할 때 쿼리가 발생하는 식이다.
  • 현재 엔티티를 조회하고, [추후 필요할 때] 연관된 엔티티를 조회하는 쿼리를 날린다.
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    private User user;
}

위 Post 엔티티는 User와 지연 로딩(LAZY) 관계를 맺고 있다. 이 경우 Post 엔티티를 조회할 때는 User 엔티티를 바로 로드하지 않고, getUser() 메서드를 호출해서 User 정보를 실제로 사용할 때 DB에서 쿼리가 실행되어 데이터를 로드한다. 장점으로는 연관된 엔티티가 많아도 불필요한 데이터는 로드하지 않아 성능을 향상시킬 수 있다. 하지만 엔티티에 접근할 때마다 추가적인 데이터베이스 쿼리가 발생할 수 있다. 이를 잘못 사용할 경우 N+1 문제를 일으킬 수 있다.

 

 

 

기본 로딩 전략 (디폴트)

  • @OneToMany, @ManyToMany: 기본적으로 지연 로딩(LAZY)으로 설정
  • @ManyToOne, @OneToOne: 기본적으로 즉시 로딩(EAGER)으로 설정
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne // 기본 즉시 로딩 (EAGER)
    private User user;

    @OneToMany(mappedBy = "post") // 기본 지연 로딩 (LAZY)
    private List<Comment> comments;
}
728x90
728x90

🧐 문제 발생 상황

OAuth2 소셜로그인을 하면 생일, 성별, 연락처에 대한 정보는 가져올 수가 없어서 추가 정보 기입 폼을 만들어서 디비에 저장되도록 구현했다.

그래서 생각한게 어느 블로그에서 본건데 Setter보다 @Builder 패턴을 사용하는게 좋다해서 소셜로그인 후 추가 정보 입력에 적용해보았다. 그래서 아래와 같은 코드로 실행을 했더니

// entity
@Getter 
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Oauth2LoginDto {

    private String phoneNumber; // 연락처
    private LocalDate birthdate; // 생년월일
    private String gender; // 성별
}
    // service
    
    /**
     * Oauth2 로그인 시 추가 정보 입력
     */
    @Override
    public Optional<User> updateOauth2(String username, Oauth2LoginDto oauth2LoginDto) {
        
        User user = User.builder()
        			.phoneNumber(oauth2LoginDto.getPhoneNumber())
				.gender(oauth2LoginDto.getGender())
				.birthdate(oauth2LoginDto.getBirthdate())
				.build();
        userRepository.save(user);
        return Optional.of(user);
    }

아래와 같은 오류가 발생했다.

org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : com.example.omg_project.domain.user.entity.User.nam

저 오류에 대해서 검색하니

 DataIntegrityViolationException 예외는 JPA가 엔티티를 저장하는 도중 User 엔티티의 name 필드가 null로 설정되어 있는 것을 감지했기 때문에 발생한 것이다. name 필드는 not-null 제약 조건이 설정되어 있어서 이로 인해 Hibernate가 엔티티를 데이터베이스에 저장할 수 없었다.

라고 한다.

 DataIntegrityViolationException 예외는 JPA가 엔티티를 저장하는 도중 User 엔티티의 name 필드가 null로 설정되어 있는 것을 감지했기 때문에 발생한 것이다. name 필드는 not-null 제약 조건이 설정되어 있어서 이로 인해 Hibernate가 엔티티를 데이터베이스에 저장할 수 없었다.

 

빌더 패턴을 사용해서 구현할 거면 아래와 같이 기존 사용자가 notnull로 갖고 있는 필드들에 대해서 다 값을 복사해줘야 한다는 것이다.. 그래서 아니 세터보다 빌더를 사용하라면서 왜 저렇게 굳이 저래야 하나..? 했는데 열심히 구글링을 해서 빌더와 세터의 차이에 대해서 확실히 이해가 갔다.

/**
 * Oauth2 로그인 시 추가 정보 입력
 */
@Override
public Optional<User> updateOauth2(String username, Oauth2LoginDto oauth2LoginDto) {

    User user = User.builder()
				.name(user.getName()) // 기존에 있던 값 추가
				.phoneNumber(oauth2LoginDto.getPhoneNumber())
				.gender(oauth2LoginDto.getGender())
				.birthdate(oauth2LoginDto.getBirthdate())
				.build();
    userRepository.save(user);
    return Optional.of(user);
}

 

이러한 문제가 발생된 원인은 내가 빌더패턴을 정확하게 이해하지 못해서 발생한 대참사이다. 빌더는 객체를 수정하는 것이 아니라 생성자처럼 객체를 생성하는 기능을 수행한다고 한다.

 

 

정확하게 알아야 할 개념

  • 빌더패턴은 객체의 생성을 담당하는 패턴으로, 일반적으로 빌더를 사용하여 객체를 새로 생성한다. 빌더를 사용하여 객체를 생성할 때, 이전 객체의 값을 수정하는 것이 아니라 새로운 객체를 생성하는 것이다.
  • 이것이 빌더 패턴의 주요 특징 중 하나이며, 이를 이해하는 것이 중요하다.
  • 빌더 패턴은 객체 생성 시에 여러 속성을 가진 객체를 편리하게 생성하기 위해 사용된다. 따라서 객체의 속성을 일부 변경하거나 해당 속성을 지정하여 새로운 객체를 생성해야한다고 한다.
  • 따라서 내가 dto로 받아온 값을 User 엔티티의 빌더 패턴으로 엔티티 객체로 변환하려고 하는 행위(?)는 잘못되었다고 할 수 있다.
  • 하지만 빌더 패턴을 사용해서 객체의 데이터를 수정하는 것이 아예 불가능한 것은 아니라고 한다.

 

 

그렇다면 빌더 패턴으로 어떻게 값을 수정할 수 있는가?

// entity
@Getter  @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true) // 기존 인스턴스를 기반으로 새로운 객체를 빌드할 수 있게 함
public class Oauth2LoginDto {

    private String phoneNumber; // 연락처
    private LocalDate birthdate; // 생년월일
    private String gender; // 성별
}

이처럼 toBuilder 속성을 true 로 설정해줄 경우 toBuilder() 메서드를 사용할 수 있게되는데

이 메서드를 사용하면 이미 생성되어있는 객체의 값을 toBuilder() 메서드를 활용하여 특정 필드의 값만을 변경해줄 수 있다고 한다.

 

 

결론

toBuilder 메서드를 활용하면 객체의 값을 일부 수정할 수는 있지만 내가 구현하려는 로직에서는 사용하기에 적합하지 않은 것 같다는 생각이 들었다.

어쨌든 빌더패턴이라는 것이 매개변수가 많아 생성자에 정의된 순서대로 데이터를 넣기 어려워 이를 편하게 하기 위한 디자인패턴인 건데 즉, 객체를 생성하는 것이 주된 목적인데 나는 객체의 값을 수정하는 로직을 만들어야 하므로 객체의 값을 부분적으로 수정하기 위해서는 굳이 엔티티 객체로 바꿀 필요는 없을 것 같고, dto에서 값을 꺼내서 entity에 setter 를 사용해서 update를 해주는 방식으로 구현하는 것이 적합한 것 같다.

 

 

✅ 정리

  • 세터 사용 방법: 기존 사용자 객체를 업데이트하는 방식이므로, 코드가 간결하고 오류 발생 가능성이 적다. 또한, 필수적으로 설정해야 하는 필드를 실수로 놓칠 가능성도 적다.
  • 빌더 사용 방법: 만약 객체의 다른 필드를 변경하거나 새로운 객체로 교체해야 하는 상황이 아니라면, 굳이 빌더 패턴을 사용할 필요는 없다. 이 방법은 필요 이상으로 복잡할 수 있으며, 잘못된 필드 값을 복사하거나 누락할 위험이 있다.
  • 즉, 빌더 패턴은 새로운 객체를 생성하거나 객체의 불변성을 유지해야 할 때 더 유용하다. 하지만 여기서는 기존 객체에 OAuth2 유저의 성별, 생일, 연락처의 정만 업데이트하는 것이므로 세터방식을 사용하는 것이 더 자연스럽고 안전하다.
728x90

+ Recent posts