[241114 TIL] AI 검증 비즈니스 프로젝트

카테고리, 가게, 리뷰 CRUD API 구현

  • 코드리뷰 수정사항 반영
  • Soft Delete 수정
  • 리뷰 CRUD 추가 기능 구현

코드리뷰 수정사항 반영

 

✔️ 카테고리 아이디 null 체크 부분 ➡️ 공백값도 같이 체크 가능하게 변경

 

  • 수정 전
if (categoryId == null) 

 

  • 수정 후
if (!StringUtils.hasText(String.valueOf(categoryId)))

 

StringUtils.hasText 이용

 

내부 로직

public static boolean hasText(@Nullable String str) {
    return str != null && !str.isEmpty() && containsText(str);
}

private static boolean containsText(CharSequence str) {
    int strLen = str.length();
    
    for(int i = 0; i < strLen; ++i) {
      if (!Character.isWhitespace(str.charAt(i))) {
        return true;
      }
    }
    
    return false;
}

 

  • str 이 null 인가
  • str이 빈 문자열 ("")인가
  • str이 공백으로만(whitespace) 이루어져 있는가 

 

스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다

 

스프링은 다음과 같은 다양한 스코프를 지원

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
  • 웹 관련 스코프
    • request웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    • session웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

싱글톤 빈 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환

  1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
  2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환
  3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환

프로토타입 스코프

 

프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환

  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
  2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입
  3.  스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환
  4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환 

 

프로토타입 빈의 특징 정리

  • 스프링 컨테이너에 요청할 때 마다 새로 생성
  • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여
  • 종료 메서드가 호출되지 않음
  • 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리
  • 종료 메서드에 대한 호출도 클라이언트가 직접 해야함

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환하지만, 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의

 

  1. 클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청
  2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01), 해당 빈의 count 필드 값은 0
  3. 클라이언트는 조회한 프로토타입 빈에 `addCount()` 를 호출하면서 count 필드를 +1
  4. 결과적으로 프로토타입 빈(x01)의 count는 1
  5. 클라이언트 B도 같은 과정을 반복하여 프로토타입 빈(x02)의 count도 1

 

싱글톤 빈에서 프로토타입 빈 사용(clientBean)

  1. clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생
    1. clientBean 은 의존관계 자동 주입을 사용한다주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
    2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환, 프로토타입 빈의 count 필드 값은 0
    3. 이제 clientBean 은 프로토타입 빈을 내부 필드에 보관 (정확히는 참조값을 보관)
  2. 클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받고, 싱글톤이므로 항상 같은 clientBean 이 반환
    1. 클라이언트 A는 clientBean.logic() 을 호출
    2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ➡︎ count값이 1
  3. 클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받고, 싱글톤이므로 항상 같은 clientBean 이 반환 (여기서 중요한 점, clientBean 이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈, 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지사용 할 때마다 새로 생성되는 것이 아님!)
    1. 클라이언트 B는 clientBean.logic() 을 호출
    2. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가 ➡︎ 래 count 값이 1이었으므로 2가 됨

 

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

  • 실행해보면 ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인
  • 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 
  • 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워짐
  • 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 됨

 

ObjectFactory, ObjectProvider

  • 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider, 참고로 과거에 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어짐
  • 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인
  • ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (DL)
  • 스프링이 제공하는 기능을 사용하지만기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워짐
  • 지금 딱 필요한 DL 정도의 기능만 제공
  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
  • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

 

JSR-330 Provider

  • JSR-330 자바 표준을 사용하는 방법 - 스프링 부트 3.0은 jakarta.inject.Provider 사용
  • 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인 
  • provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (DL)
  • 자바 표준이고기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워짐
  • Provider 는 지금 딱 필요한 DL 정도의 기능만 제공
  • get() 메서드 하나로 기능이 매우 단순
  • 별도의 라이브러리가 필요
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능

 

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리, 따라서 종료 메서드가 호출

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

빈 생명주기 콜백

애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요

 

  • 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료됨
  • 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 함
  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능 제공
  • 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 줌

 

스프링 빈의 간단한 라이프사이클

  • 객체 생성 ➡︎ 의존관계 주입

스프링 빈의 이벤트 라이프사이클

  • 스프링 컨테이너 생성 ➡︎ 스프링 빈 생성 ➡︎ 의존관계 주입 ➡︎ 초기화 콜백 ➡︎ 사용 ➡︎ 소멸전 콜백 ➡︎ 프링 종료
    • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
    • 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

✚ 객체의 생성과 초기화를 분리

  • 생성자: 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임
  • 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행

 


빈 생명주기 콜백의 3가지 방법

  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. @PostConstruct, @PreDestroy 애노테이션 지원

 

1. 인터페이스(InitializingBean, DisposableBean)

  • 초기화: InitializingBean - afterPropertiesSet() 메서드
  • 소멸: DisposableBean - destroy() 메서드
public class NetworkClient implements InitializingBean, DisposableBean {
    
    ...
    중략
    ...
    
    @Override
    public void afterPropertiesSet() throws Exception {
    	connect();
        call("초기화 연결 메시지"); 
    }
    
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}

 

단점

  • 초기화, 소멸 메서드의 이름 변경 불가
  • 외부 라이브러리에 적용 불가
  • 초창기 방법이라 거의 사용하지 않음

 

 

2. 빈 등록 초기화, 소멸 메서드 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지

 

public class NetworkClient {
	
    ...
    중략
    ...
    
    public void init() { 
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
	
    public void close() {
    	System.out.println("NetworkClient.close");
        disConnect();
    }
}
@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
    	...
    }
}

 

특징

  • 메서드 이름 자유로움
  • 스프링 빈이 스프링 코드에 의존하지 않음
  • 설정 정보를 사용하기에 외부 라이브러리에도 적용 가능

 

 

3. 애노테이션 @PostConstruct, @PreDestroy 

public class NetworkClient {
	
    ...
    중략
    ...
    
    @PostConstruct
    public void init() { 
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
    
    @PreDestroy
	public void close() {
 		System.out.println("NetworkClient.close");
 		disConnect();
    }
}

 

특징

  • 가장 편리하게 초기화와 종료를 실행
  • 최신 스프링 권장 방법
  • 컴포넌트 스캔과 잘 어울림
  • 외부 라이브러리에 적용 불가

 

 

의존관계 자동 주입

스프링은 의존관계도 자동으로 주입하는 @Autowired 라는 기능 제공

 

@Autowired 의존관계 자동 주입 동작

  • 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입
    • getBean(MemberRepository.class) 와 동일하다고 이해하면 됨

 

다양한 의존관계 주입 방법

@Component
public class OrderServiceImpl implements OrderService {
    
    // 1. 생성자 주입
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
     
     
    // 2. 수정자 주입
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
    
    
    // 3. 필드 주입
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
    
    
    // 4. 일반 메서드 주입
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
}
  1. 생성자 주입
    • 생성자를 통해서 의존관계를 주입받는 방법
    • 생성자 호출 시점에 딱 1번만 호출되는 것 보장
    • 불편, 필수 의존관계에 사용
    • 생성자가 딱 1개만 있으면 @Autowired 생략 가능
  2. 수정자 주입(setter 주입)
    • setter 라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계 주입하는 방법
    • 선택, 변경 가능성이 있는 의존관계에 사용
  3. 필드 주입
    • 필드에 바로 주입하는 방법
    • 코드가 간결
    • 외부에서 변경이 불가능해서 테스트하기 힘듦
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드/스프링 설정을 목적으로 하는 @Configuration 같은 특별한 용도로만 사용
  4. 일반 메서드 주입
    • 일반 메서들르 통해 주입받는 방법
    • 한번에 여러 필드를 주입 받을 수 있음
    • 일반적으로 잘 사용하지 않음

 

✚ 주입할 스프링 빈이 없어도 동작해야 할 때, 자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력

 

✚ 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동 생성

 

 

✚ 조회 대상 빈이 2개 이상일 때 해결 방법

 

  • @Autowired 필드 명 매칭
  • @Qualifier ➡︎ @Qualifier 끼리 매칭 ➡︎ 빈 이름 매칭 
  • @Primary 사용

 

‼️ 생성자 주입 권장, 옵션이 필요하면 수정자 주입, 필드 주입은 XXX ‼️

‼️ 최근에는 생성자를 딱 1개 두고, @Autowired 생략 &   @RequiredArgsConstructor 함께 사용 ‼️

 

컴포넌트 스캔

스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 @ComponentScan 이라는 기능을 제공

 

@ComponentScan 컴포넌트 스캔 동작

  • @ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용
    • 빈 이름 기본 전략: MemberServiceImpl 클래스 ➡︎ memberServiceImpl
    • 빈 이름 직접 지정:  @Component("memberService2")이런식으로 이름을 부여

탐색 위치와 기본 스캔 대상 

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸림 ➡︎ 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정

@ComponentScan(
         basePackages = "hello.core",
 }
  •  basePackages: 탐색할 패키지의 시작 위치를 지정 - 이 패키지를 포함해서 하위 패키지를 모두 탐색
  •  basePackageClasses지정한 클래스의 패키지를 탐색 시작 위치로 지정
  •  지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치

‼️ 권장하는 방법
설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것

 


 

컴포넌트 스캔 기본 대상 및 부가 기능

  • @Component: 컴포넌트 스캔에서 사용
  • @Controller: 스프링 MVC 컨트롤러에서 사용 / 스프링 MVC 컨트롤러로 인식
  • @Service: 스프링 비즈니스 로직에서 사용 / 특별한 처리를 하지 않지만, 대신 개발자들이 비즈니스 계층을 인식하는데 도움
  • @Repository스프링 데이터 접근 계층에서 사용 / 스프링 데이터 접근 계층으로 인식데이터 계층의 예외를 스프링 예외로 변환
  • @Configuration스프링 설정 정보에서 사용 / 스프링 설정 정보로 인식하고스프링 빈이 싱글톤을 유지하도록 추가 처리

 

필터

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters컴포넌트 스캔에서 제외할 대상을 지정

 

FilterType 옵션

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작 ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작 ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용 ex) org.example..*Service+
  • REGEX: 정규 표현식 ex) `org\.example\.Default.*`
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리 ex) org.example.MyTypeFilter

 

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록

 

  • 자동 빈 등록 vs 자동 빈 등록
    • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생
      • ConflictingBeanDefinitionException 예외 발생
  • 수동 빈 등록 vs 자동 빈 등록
    • 수동 빈 등록이 우선권 - 수동 빈이 자동 빈을 오버라이딩

 

‼️ 같은 빈 이름 X 중요 - 스프링 부트는 수동 빈/자동 빈 충돌 시 오류 발생

웹 애플리케이션과 싱글톤

 
 

  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청

스프링 없는 순수한 DI 컨테이너

  • 요청할 때 마다 객체를 새로 생성 ➡︎ 메모리 낭비가 심함
    ➡️ 싱글톤 패턴으로 설계

싱글톤 패턴

: 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴

 

객체 인스턴스를 2개 이상 생성하지 못하도록 막기

  1. static 영역에 객체 instance를 미리 하나 생성
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회 -  이 메서드를 호출하면 항상 같은 인스턴스를 반환
  3. 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막음

 

 

➡️ 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용

 

⚠️ 싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어감
  • 의존관계상 클라이언트가 구체 클래스에 의존 ➡︎ DIP를 위반
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성 ↑
  • 테스트하기 어렵고, 내부 속성을 변경하거나 초기화 하기 어려움
  • private 생성자로 자식 클래스를 만들기 어려움
  • 결론적으로 유연성이 떨어져 안티패턴으로 불리기도 함

 

싱글톤 컨테이너 (스프링 컨테이너)

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 함 -  싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리
  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 됨
  • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용 가능

 싱글톤 컨테이너 적용

  • 이미 만들어진 객체를 공유해서 효율적으로 재사용

 


싱글톤 방식의 주의점

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안됨

 

무상태(stateless) 설계

  • 특정 클라이언트에 의존적인 필드가 있으면 안됨
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨
  • 가급적 읽기만 가능
  • 필드 대신에 자바에서 공유되지 않는지역변수파라미터, ThreadLocal 등을 사용

 


Configuration 과 바이트코드 조작

  • @Configuration 을 붙이면 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록

 

AppConfig@CGLIB 예상 코드

@Bean
public MemberRepository memberRepository() {
	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
    	return 스프링 컨테이너에서 찾아서 반환;
	} else { //스프링 컨테이너에 없으면
		기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
        return 반환
	}
}

 

 

 

‼️ 스프링 설정 정보는 항상 @Configuration 사용 ‼️

+ Recent posts