개발/부트캠프

본캠프 : Spring 숙련_1주차

EJ EJ 2025. 2. 4. 17:02

▶ SOLID 원칙

객체 지향 설계의 5가지 기본 원칙, 소프트웨어 설계에서 유지보수성, 확장성, 유연성을 높이기 위한 지침을 제공한다.

 

-SOLID 원칙의 종류

1.단일 책임 원칙 SRP(Single Responsibility Principle)

: 하나의 클래스는 하나의 책임만 가져야 한다.

클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 한다

 

2.개방 폐쇄 원칙 OCP(Open Closed Principle)

: 소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.

새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장할 수 있도록 설계해야 한다.

 

3.리스코프 치환 원칙 LSP(Liskov Substitution Principle)

: 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.

부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.

 

4.인터페이스 분리 원칙 ISP(Interface Segregation Principle)

: 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.

즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.

 

5.의존관계 역전 원칙 DIP(Dependency Inversion Principle)

: 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.

 

💡 객체 지향의 핵심은 다형성에 있다. 하지만 다형성 만으로는 OCP, DIP를 지킬 수 없다.

 

▶ Spring과 객체 지향

Spring은 다형성 만으로는 해결하지 못했던 객체 지향 설계 원칙 중 OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다.

 

  • Spring의 역할
    • OCP, DIP 원칙을 지킬 수 있도록 도와준다.
    • 코드의 변경 없이 기능을 확장할 수 있도록 만들어 준다.
    • 개발자가 마치 레고 블록을 조립하듯이 원하는 구성 요소를 손쉽게 교체하고 결합할 수 있도록 만들어준다.
  • 다시보는 Spring Framework 의 등장 배경
    • OCP, DIP 원칙을 지키며 개발하면 개발자가 할일이 아주 많아진다.
    • Framework로 만들어서 개발자가 편하게 개발할 수 있도록 만들어졌다.
  • 개발자의 역할
    • 이상적으로는 모든 설계를 인터페이스로 만들어야 코드가 유연하게 변경이 가능해진다.
      • 정해진 비지니스 로직이나 사용할 기술이 없는 상황에서도 개발할 수 있는 장점이 생긴다.

💡 실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 하면된다.

 

▶ Spring Container

Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당한다. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤한다.

  • 객체를 직접 생성하는 경우, 객체 간의 의존성 및 결합도가 높아진다. -> OCP, DIP 위반
  • Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해진다. -> OCP, DIP 준수

-Spring Container의 종류

  • BeanFactory
    • Spring Container의 최상위 인터페이스
    • Spring Bean을 관리하고 조회한다.
  • ApplicationContext
    • BeanFactory의 확장된 형태(implements)
    • Application 개발에 필요한 다양한 기능을 추가적으로 제공한다. 국제화, 환경변수 분리, 이벤트, 리소스 조회

💡 일반적으로 ApplicationContext를 사용하기 때문에 ApplicationContext를 Spring Container라 표현한다.

 

▶ Spring Bean

Spring Container가 생성하고 관리하는 Java 객체를 의미한다. 자바 객체 자체는 특별하지 않지만, Spring이 이 객체를 관리하는 순간부터 Bean이 된다. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리한다.

  • Bean은 new 키워드 대신 사용하는 것이다.
  • Spring Container가 제어한다.

-Spring Bean의 특징

  • Spring 컨테이너에 의해 생성되고 관리된다.
  • 기본적으로 Singleton으로 설정된다.
  • 의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있다.
  • 생성, 초기화, 사용, 소멸의 생명주기를 가진다.

-Bean 등록 방법

  • XML, Java Annotation(@Component, @ComponentScan), Java 설정파일 등을 통해 Bean으로 등록할 수 있다.

▶ IOC / DI

  1. IoC는 객체의 제어권을 개발자가 아닌 Spring 컨테이너에게 넘기는 개념으로, Spring이 객체 생성과 관리를 담당한다.
  2. DI는 Spring이 객체 간의 의존성을 자동으로 주입해주는 기법이다.
  3. 의존관계 주입은 객체 간의 결합도를 낮추고 코드의 유연성과 테스트 가능성을 높여준다.

▶ 싱글톤 패턴(Singleton Pattern)

클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴이다. 객체가 한번만 생성되어 리소스를 절약할 수 있다.

 

-싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하기 위한 코드의 양이 많다.
  • 구현 클래스에 의존해야 한다.(DIP, OCP 위반)
  • 유연성이 떨어져서 안티패턴으로 불리기도 한다.

-싱글톤 패턴의 주의점

객체의 인스턴스를 하나만 생성하여 공유하는 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안된다.

  • 상태 유지(stateful)의 문제점 -> 데이터의 불일치나 동시성 문제가 발생할 수 있다.

💡 Spring Bean은 항상 무상태(stateless)로 설계를 해야한다. 아주 중요!

특정 클라이언트에 의존적인 필드가 있거나 변경할 수 있으면 안된다.

 

-Spring의 싱글톤 컨테이너

  • Spring은 Web Application을 만들 때 주로 사용된다.
  • Spring Container는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리한다.
  • Spring Bean은 싱글톤으로 관리되는 객체이다.

💡 Spring이 Bean을 등록하는 방법은 기본적으로 싱글톤이다. 하지만, 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

 

▶ Spring Bean 등록

Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller 같은 Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능이다. 개발자가 Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾는다.

 

  • @ComponentScan의 동작 순서
    1. Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색한다.
    2. 해당 패키지에서 @Component 또는 Annotation이 붙은 클래스를 찾습니다.
    3. 찾은 클래스를 Spring 컨테이너에 빈으로 등록합니다.
    4. 등록된 빈은 **의존성 주입(DI)**과 같은 방식으로 다른 빈과 연결됩니다.

-Spring Bean을 등록하는 방법에는 수동, 자동 두가지가 존재한다.

  Spring Bean은 Bean의 이름으로 등록된다.

 

    1.자동 Bean 등록(@ComponentScan, @Component)

  • @Component 이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록한다.
  • @ComponentScan 을 통해 @Component로 설정된 클래스를 찾는다.

    2.수동 Bean 등록(@Configuration, @Bean)

  • @Configuration 이 있는 클래스를 Bean으로 등록하고 해당 클래스를 파싱해서 @Bean 이 있는 메서드를 찾아 Bean을 생성한다. 이때 해당 메서드의 이름으로 Bean의 이름이 설정된다.

💡수동으로 Bean을 등록할 때는 항상 @Configuration과 함께 사용해야 Bean이 싱글톤으로 관리된다. CGLIB 라이브러리와 연관이 있다.

 

  • 수동 Bean 등록이 자동 Bean 등록을 오버라이딩해서 우선권을 가진다.
  • 의도한 결과라면 다행이지만, 아닌 경우(실수)가 대부분이다. → 버그 발생
  • Spring Boot에서는 수동과 자동 Bean등록의 충돌이 발생하면 오류가 발생한다.

⛔ 주의사항

  • 테스트 시 자동 빈 등록 충돌 conflict 패키지의 Bean Name을 지워주세요.
  • 테스트 시 반드시 @SpringBootApplication 으로 실행해야 합니다.

💡Annotation 기반의 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용한다. @Component 뿐만 아니라 @Controller, @Service, @Repository 등 자동으로 쉽게 등록할 수 있는 Annotation들을 지원하고 Spring Boot는 ComponentScan 방식을 기본으로 사용한다.

 

▶ 의존관계 주입

의존관계 주입을 하는 방법으로 생성자 주입, setter 주입, 필드 주입, 메서드 주입 총 4가지 방법이 존재한다.

 

-의존관계 주입 방법

@Autowired 는 의존성을 자동으로 주입할 때 사용하는 Annotation이다.

기본적으로 주입할 대상이 없으면 오류가 발생한다.(required = true)

 

1.생성자 주입

  • 생성자를 통해 의존성을 주입하는 방법.
  • 최초에 한번 생성된 후 값이 수정되지 못한다.[불변, 필수]
  • 생성자가 하나인 경우 @Autowired 생략이 가능하다.(생성자가 두개인 경우 생략이 불가능하다. 둘중 어떤 생성자를 사용해야 하는지 Spring은 알지 못한다.)

2.Setter 주입

  • Setter 메서드를 통해 의존성을 주입하는 방법
  • 선택하거나, 변경 가능한 의존관계에 사용한다.(생성자 주입은 필수 값)

3.필드 주입

  • 필드에 직접적으로 주입하는 방법(가장 추천되지 않음)
  • 코드는 간결하지만 Spring이 없으면 사용할 수 없다. 사용하지 않아야 한다.
  • Application의 실행과 관계 없는 @SpringBootTest 테스트 코드나 Spring에서만 사용하는 @Configuration 같은 곳에서 주입할 때 주로 사용한다.

4.일반 메서드 주입

  • 생성자, setter 주입으로 대체가 가능하기 때문에 사용하지 않는다.

💡의존관계를 자동으로 주입할 객체가 Spring Bean으로 등록되어 있어야 @Autowired 로 주입이 가능하다.

 

▶ 생성자 주입

과거 setter, 필드 주입도 사용했지만 현재는 DI를 가지고 있는 대부분의 Framework가 생성자 주입 방식을 권장한다.

 

-불변(immutable) :

  • 객체를 생성할 때 최초 한번만 호출된다.
  • setter 주입을 사용하면 접근제어자가 public 으로 설정되어 누구나 수정할 수 있게된다.

-실수 방지 :

  • 순수 Java 코드로 사용할 때(주로 테스트 코드) 생성자의 필드를 필수로 입력하도록 만들어준다.(NPE 방지)
  • 컴파일 시점에 오류를 발생 시킨다. 즉, 실행 전에 오류를 알 수 있다.

-Spring Framework에 의존하지 않아도 객체 지향 특성을 가장 잘 사용하는 방법이다.

 

💡필드에 final 은 생성자 주입 방식만 사용할 수 있다. 나머지 주입 방식들은 모두 생성 이후에 호출되어 사용할 수 없다.

 

▶ @RequiredArgsConstructor

실제 Web Application을 개발하면 대부분이 불변 객체이고 생성자 주입 방식을 선택하게 된다. 이런 반복되는 코드를 편안하게 작성하기 위해 Lombok에서 제공하는 Annotation 이다.

  • final 필드를 모아서 생성자를 자동으로 만들어 주는 역할
  • Annotation Processor 가 동작하며 컴파일 시점에 자동으로 생성자 코드를 만들어준다.
  • 생성자를 하나 만들고 @Autowired 를 사용한 코드와 똑같이 동작한다.

▶ @Primary

같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 Annotation

@Primary로 지정된 Bean이 우선 순위를 가진다.

-실제 적용 사례

  • Database가 (메인 MySQL, 보조 Oracle) 두개 존재하는 경우
    • 기본적으로 MySQL을 사용할 때 @Primary를 사용하면 된다.
    • 필요할 때 @Qualifier로 Oracle을 사용하도록 만들 수 있다.
    • 동시에 사용되는 경우 @Qualifier 의 우선순위가 높다.

▶ Validation(검증)

특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계를 의미한다.

Controller의 주요한 역할 중 하나는 Validation 이다. HTTP 요청이 정상인지 검증한다.

  • Validation의 역할
    1. 검증을 통해 적절한 메세지를 유저에게 보여주어야 한다.
    2. 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 한다.
    3. 사용자가 입력한 데이터는 유지된 상태여야 한다.

💡기본적으로 프론트, 서버, 데이터베이스 모두 검증을 꼼꼼하게 하는것이 바람직하다. Validation으로 수많은 Error와 문제들을 방지할 수 있다.

 

▶ BindingResult

Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체이다. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관한다. BindingResult 파라미터는 검증대상 파라미터 뒤에 위치해야만 한다. BindingResult에 오류가 보관되고 Controller는 정상적으로 호출된다.

  • Errors 인터페이스를 상속받은 인터페이스이다.
  • Errors 인터페이스는 에러의 저장과 조회 기능을 제공한다.
  • BindingResult는 addError() 와 같은 추가적인 기능을 제공한다.
  • Spring이 기본적으로 사용하는 구현체는 BeanPropertyBindingResult 이다.

💡ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다. 파라미터에 Binding Result가 함께 있는 경우 만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출된다.

 

▶ Bean Validation

특정 필드 검증의 경우 빈값, 길이, 크기, 형식 과 같은 간단한 로직이다. 이러한 로직들을 모든 프로젝트에 적용할 수 있도록 표준화 한 것이 Bean Validation이다.

 

-객체의 필드나 메서드에 제약 조건을 설정하여, 올바른 값을 가지고 있는지 검증하는 표준화된 방법

  • Bean Validation은 기술 표준 인터페이스이다.
  • 다양한 Annotation들과 여러가지 Interface로 구성되어 있다. Bean Validation(인터페이스) 구현체인 Hibernate Validator를 사용한다.
  • Annotation을 적용시키는것 만으로 Validation을 아주 쉽게 적용할 수 있다.
  • Controller에 개발자가 기본적인 검증 로직을 작성할 필요가 없어졌다.
  • RestController의 @RequestBody에도 사용할 수 있다.

▶ Field Error

  • @NotBlank

null을 허용하지 않는다.

공백(” “)을 허용하지 않는다. 하나 이상의 문자를 포함해야한다.

빈값(””)을 허용하지 않는다.

CharSequence 타입 허용, String은 CharSequence(Interface)의 구현체이다.

 

  • @NotNull

null을 허용하지 않는다.

모든 타입을 허용한다.

 

  • @NotEmpty

null을 허용하지 않는다.

빈값(””)을 허용하지 않는다.

CharSequence, Collection, Map, Array 허용

 

-Validator

 

단순히 Annotation을 선언해주면 검증이 완료되는 이유는 Validator(Validation을 사용하는것)가 존재하기 때문이다. Spring Boot는 validation 라이브러리를 설정하면 'org.springframework.boot:spring-boot-starter-validation'자동으로 Bean Validator를 Spring에 통합되도록 설정해준다.

 

  • @Valid, @Validated 차이점
    1. @Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 Annotation이다.
    2. @Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.
    3. @Valid 는 MethodArgumentNotValidException 예외를 발생시킨다.
    4. @Validated 는 ConstraintViolationException 예외를 발생시킨다.

-에러 메세지

Spring의 Bean Validation은 Default로 제공하는 Message들이 존재하고 임의로 수정할 수 있다.

 

▶ Object Error

필드 단위가 아닌 객체 전체에 대한 오류를 나타낸다. 예를들어 두 필드 간의 관계를 검증할 때 ObjectError를 통해 해당 오류를 BindingResult에 기록할 수 있다.

 

▶ Bean Validation의 충돌

등록, 수정 API에서 각각 다른 Validation이 적용된다면?

  • 해결방법
    1. 저장할 Object를 직접 사용하지 않고 SaveRequestDto, UpdateRequestDto 따로 사용한다.
    2. Bean Validation의 groups 기능을 사용한다.

▶ groups

Bean Validation의 groups 속성은 다양한 유효성 검사 시나리오를 정의할 때 사용된다. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 groups를 활용할 수 있다.

 

▶ groups VS DTO 분리

Bean Validation의 충돌이 발생하는 경우 대부분 DTO를 분리하는 방법이 적절하다.

  • DTO 분리
    • 실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있다.
    • 각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있다.
    • RequestDto가 변한다는건 해당 API의 스펙 자체가 변경되어 많은 수정이 발생한다.
    • 실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하도록 하자!

▶ @ModelAttribute, @RequestBody

@Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용할 수 있다. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용한다.

 

-@ModelAttribute와 @RequestBody 차이점

  1. @ModelAttribute
    • 각각의 필드 단위로 바인딩한다.
    • 특정 필드 바인딩이 실패하여도 나머지 필드는 정상적으로 검증 처리할 수 있다.
    • 특정필드 변환 실패
      • 컨트롤러 호출, 나머지 필드 Validation 적용
  2. @RequestBody
    • 필드별로 적용되는것이 아니라 객체 단위로 적용된다.
    • MessageConverter가 정상적으로 동작하여 Object로 변환하여야 Validation이 동작한다.
    • 특정필드 변환 실패
      • 컨트롤러 미호출, Validation 미적용