Spring 숙련 1주차
Keyword
- 객체 지향- SOLID 원칙
- Spring Container, Spring Bean
- IOC/DI
- Singleton Pattern
- Validation
- Bean Validation
객체 지향 설계 - 5원칙
- S (SRP: Single Responsibility Principle)
- 하나의 클래스는 하나의 책임만. 클래스는 한 가지 기능에 집중.
> User 클래스 내 이름, 저장, 로그인 이렇게 포함하면 안됨
> AuthService에 Login을 떼고 / UserRepository에 saveUser를 떼야함
- O (OCP: Open Closed Principle)
- 확장에 열려 있고, 수정에는 닫혀 있어야 한다.
- 새로운 기능 추가 시 기존 코드를 수정하지 않고, 확장할 수 있도록 설계.
- CalculateArea 클래스 예시
> Shape() 인터페이스 - double calculateArea() 메서드 정의
> Circle / Square Class 인터페이스 상속
> AreaCalculator 클래스 내
> calculate(Shape shape) / return shape.calculateArea(); 메서드 정의
- L (LSV: Liskov Substitution Principle)
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
- Car / ElectricCar 예시
> Acceleratable interface 내 void accelerate() 선언
> Car / ElectricCar class 개별 선언 / accelerate 개별 정의
- I (ISP: Interface Segregation Principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나 보다 낫다.
- 예시: Animal interface 내 fly, run, swin 이렇게 두는 것 권장X
- interface Runnable void run() / interface Swimmable void swim()
- 이렇게 따로 떼어 놓고 다중 상속(implements)해서 클래스 정의하는 게 낫다!
- D (DIP : Dependency Inversion Principle)
- 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계
> 예) Interface Notifier void send(String message) 인터페이스 정의 후
> Class EmailNotifier implements Notifier void send(String message)
> Class SNSNotifier implements Notifier void send(String message)를 각각 정의
> 서비스 클래스에서 Notifier를 속성으로 갖고, 생성자 주입, void sendNotification(String message) notifier.send(message)와 같이 구현해놓고
Spring과 객체 지향
- Spring의 역할 : OCP, DIP 원칙을 지킬 수 있도록 도움 / 코드 변경 없이 기능 확장할 수 있도록 만들어줌
Spring의 핵심 개념 - Spring과 객체 지향
- Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당.
- 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤
- Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해짐
- Spring 컨테이너가 관리하는 객체.
- Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리
- 의존성 주입(DI)를 통해 다른 객체들과 의존 관계를 맺을 수 있음
- XML, Java Annotation, Java 설정파일 등을 통해 Bean으로 등록 가능
- Java Annotation을 추천! @ComponentScan으로 Component를 Scan할 클래스들을 찾고(경로설정)
- @Controller / @Service / @Repository / @Component와 같은 Annotation이 붙은 클래스들을 찾음
IOC/DI
- IOC(Inversion Of Control) : 객체의 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당
- 객체 간의 결합도를 낮춰 유연한 코드가 됨
- DI (Dependency Injection) : Spring이 객체 간의 의존성을 자동으로 주입. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식. IOC를 구현하는 방식 중 하나임.
- 원래 개발자가 new로 키워드를 생성하고 넣어줘야 했는데,
- Spring은 Bean이 자동으로 등록하고 주입
- 이 개념이 잘 와닿지 않아 아래 비교 참고(Dr.G)

- MyRepository 객체 repo를 생성하고, MyServiceImpl(repo)를 넣어서 MyService 객체 service를 만듦
- 구현체를 바꾸려면 repo를 생성하는 코드를 직접 수정해야함
- Spring은 MyService 클래스를 만들 때 Autowired로 필요한 의존성 주입(MyRepository)
- 구현체를 바꾸려면 등록된 Bean만 바꾸면 됨.
- 쉽게 말해, Spring에서는 "객체를 만드는 일"도, "그 객체를 연결해주는 일"도 우리가 하지 않아도 된다는 것!



- 이것만 잘 이해해도 잘 설계할 수 있을 것 같다! Spring의 핵심이 여기에 있는게 아닐까. Bean을 만들어 생성/관리할 수 있다는 점.
Singleton Pattern
- 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴
- Web Application은 불특정 다수의 고객이 동시에 많은 요청을 보냄
- 요청을 할 때 마다 객체가 새로 생성되고 처리가 완료되면 소멸됨 -> 메모리 낭비
- 싱글톤 패턴 적용 -> 객체 인스턴스가 하나만 생성되고 생성된 인스턴스만 사용하도록 만듦
- 객체가 한 번만 생성되어 리소스를 절약할 수 있음
- Spring Container는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리
- Spring Bean은 싱글톤으로 관리되는 객체임
- 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안됨!
Spring Bean 등록
- Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller 같은 Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능.
- 개발자가 Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾음
- 여러 속성으로 스캔 기준 설정 가능
- 동작순서
> 1. Spring Application이 실행되면 @ComponentScan이 지정된 패키지 탐색
> 2. 해당 패키지에서 @Component 또는 Annotation이 붙은 클래스 탐색
> 3. 찾은 클래스를 Spring 컨테이너에 빈으로 등록
> 4. 등록된 빈은 의존성 주입(DI)와 같은 방식으로 다른 빈과 연결
- Spring Bean을 등록하는 방법은 수동, 자동 두 가지가 존재
- 1. 자동 Bean 등록(@ComponentScan, @Component)
> @Component이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록
> @ComponentScan을 통해 @Component로 설정된 클래스를 찾음
- 2. 수동 Bean 등록(@Configuration, @Bean)
> Configuration이 있는 클래스를 Bean으로 등록
> 해당 클래스를 파싱해서 @Bean이 있는 메서드를 찾아 Bean을 생성
> 이때 해당 메서드의 이름으로 Bean의 이름이 설정
> 수동으로 Bean을 등록할 때는 항상 @Configuration과 함께 사용해야 Bean이 싱글톤으로 관리됨.
- 이름이 같은 Bean이 설정되고자 한다면 충돌이 발생
- 수동 Bean 등록이 자동 Bean 등록을 오버라이딩해서 우선권을 가짐
- Spring Boot에서는 수동과 자동 Bean 등록의 충돌이 발생하면 오류 발생
- application.properties에서 overriding=true로 수동 오버라이딩 설정 가능
의존관계 주입 : 생성자 주입 / setter 주입 / 필드 주입 / 메서드 주입
- 가장 많이 활용됨(추천)
- 생성자로 의존성을 주입 / 한번 생성 후 수정 불가(불변/필수)
- 필드(속성)에 final 키워드 필수! 무조건 값이 있도록 만들어줌
- @Autowired는 의존성을 자동으로 주입할 때 사용하는 Annotation
- 기본적으로 주입할 대상이 없으면 오류 발생(required=true)
- @Autowired를 생략하려면 생성자가 하나여야 함.
- 의존관계를 자동으로 주입할 객체가 Spring Bean으로 등록되어 있어야 @Autowired로 주입이 가능함!
- 많이 활용되진 않음
- 생성자 없이 세터를 정의해 주입하는 방식 (with @Autowired)
- 선택하거나, 변경 가능한 의존관계에 사용(생성자 주입은 필수값)
- 세터니까 필드(속성)을 final로 정의하지 않네
- 필드(속성)에 @Autowired 직접 주입 ... 비추천(Spring이 없으면 사용 불가)
- 생성자, setter 주입으로 대체 가능해 사용하지 않음
- 생성자 주입 : 과거 setter, 필드 주입도 사용했지만 현재는 DI를 가지고 있는 대부분의 Framework가 생성자 주입 방식을 권장
<장점>
- 불변: 객체 생성 시 최초 한 번만 호출(불변), setter 주입 시 접근제어자가 public으로 설정되어 누구나 수정 가능해짐
- 실수 방지 : 순수 Java 코드로 사용할 때(주로 테스트 코드) 생성자의 필드를 필수로 입력하도록 만들어줌(NPE 방지) (컴파일 시점에 오류 발생 -> 실행 전에 오류를 알 수 있음)
- 실제 Web Application 개발 시 대부분이 불변 객체이고 생성자 주입 방식을 선택하게 됨.
- 이런 반복 코드를 편안하게 작성하기 위해 Lombok에서 제공하는 Annotation
- final 필드를 모아서 생성자를 자동으로 만들어 주는 역할
- Annotation Processor 가 동작하며 컴파일 시점에 자동으로 생성자 코드를 만들어줌.
Spring Bean 등록 2
- @Qualifier, @Primary ... 같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 Annotation
[같은 타입의 Bean 충돌 해결 방법]
1. @Autowired + 필드명
2. @Qualifier 사용 ... Bean 등록 시 추가 구분자를 붙여줌
> (@Qualifier("firstService) / @Qualifier("secondService") 이런식
3. @Primary 사용 ... @Primary로 지정된 Bean이 우선 순위를 가짐
> ex) Database가 두 개(메인 MySQL, 보조 Oracle) 존재할 때
> 기본적으로 MySQL을 사용할 때 @Primary
> 필요할 때 @Qualifier로 Oracle을 사용하도록 만들 수 있음
> 동시 사용 시 @Qualifier의 우선순위가 높음
- Annotation 기반의 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용
- @Component 뿐만 아니라 @Controller, @Service, @Repository 등 자동으로 쉽게 등록할 수 있는 Annotation들을 지원하고 Spring Boot는 ComponentScan 방식을 기본으로 사용
- 수동 Bean을 사용하는 경우
> 외부 라이브러리나 객체를 Spring으로 등록할 때
> 데이터베이스 연결과 같이 비즈니스 로직을 지원하는 기술
> 같은 타입의 Bean 여러 개 중 하나를 명시적으로 선택해야 할 때
- 꼭 필요한 경우가 아니라면 자동 Bean 등록을 사용하면 됨
Validation
- 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계
- Controller의 주요한 역할 중 하나. HTTP 요청이 정상인지 검증
- Validation의 역할
> 1. 검증을 통해 적절한 메시지를 유저에게 보여주어야 함
> 2. 검증 오류로 인해 정삭적인 동작을 하지 못하는 경우는 없어야 함
> 3. 사용자가 입력한 데이터는 유지된 상태여야 함
- 검증의 종류
> 1. 프론트 검증
>> 유저가 조작할 수 있어 보안에 취약하지만 꼭 필요
>> ex) 비밀번호에 특수문자가 포함돼야 한다면 즉각적인 alert 가능->유저 사용성 증가
> 2. 서버 검증
>> API Spec을 정의해 Validation 오류를 Response 예시에 남겨줘야
>> (API 명세서를 잘 만들어야 그에 맞는 대응 가능)
>> 필수다!
> 3. 데이터베이스 검증
>> Not Null, Default와 같은 제약 조건을 설정
>> 최종 방어선의 역할 수행
- Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체.
- 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관
- Errors 인터페이스를 상속받은 인터페이스
- Errors 인터페이스는 에러의 저장과 조회 기능 제공
- BindingResult는 addError()와 같은 추가적인 기능 제공
- 파라미터에 BindingResult가 없는 경우 검증 오류가 발생하고 Controller 미호출
- 파라미터에 BindingResult가 있는 경우 BindingReulst에 오류가 보관되고 Controller는 정상적으로 호출
- BindingResult는 검증대상 파라미터 뒤에 위치해야 함
- @ModelAttribute는 파라미터를 필드 하나하나에 바인딩함
- 파라미터에 Binding Result가 함께 있는 경우 만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출됨
Bean Validation 1
- 특정 필드 검증의 경우 빈값, 길이, 크기, 형식과 같은 간단한 로직임
- 이 로직들이 모든 프로젝트에 적용할 수 있도록 표준화한 것이 Bean Validation
- 객체의 필드나 메서드에 제약 조건을 설정해 올바른 값을 가지고 있는지 검증하는 표준화된 방법
- Controller에 개발자가 기본적인 검증 로직을 작성할 필요X
- RestControllerdml @RequestBody에도 사용 가능
- build.gradle에서 의존성 추가 필요 implementation 'org.springframework.boot:spring-boot-starter-validation'
- ExternalLibraries에서 사용할 수 있는 다양한 Annotation들을 확인 가능
- @NotBlank : null 허용 X / 공백(" ") 허용 X / 빈값("") 허용 X / CharSequence 타입 허용(String은 CharSequence(Interface)의 구현체임)
- @NotNull : null 허용 X / 모든 타입 허용
- @NotEmpty : null 허용 X / 빈값("") 허용X / CharSequence, Collection, Map, Array 허용
- import가 org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능으로 다른 구현체로 validator를 고쳐하면 동작X 하지만 org.hibernate.validator를 대부분 사용함
- 단순히 Annotation을 선언하면 검증이 완료되는 이유는 Validator(Validation을 사용하는 것)가 존재하기 때문
- SpringBoot는 validation 라이브러리를 설정하면 'org.springframework.boot:spring-boot-starter-validation'이 자동으로 Bean Validator를 Spring에 통합되도록 설정
- 동작순서
> 1. LocalValidatorFacotryBean을 Global Validator로 등록
> 2. Global Validator가 Default로 적용되어 있어, @Valid, @Validated만 적용하면 됨
> 3. Bean Validation Annotation이 있으면 검증 수행 (ex. @NotNull, @NotBlank, @Max 등등..)
> 4. Validation Error가 발생하면 FieldError, ObjectError를 생성하여 BindingResult에 담아줌
- @Valid, @Validated 차이점
> 1. @Valid는 JAVA 표준 / @Validated는 Spring에서 제공하는 Annotation
> @Validated를 통해 **Group Validation 혹은 Controller 이외 계층에서 Validation이 가능** ... 아 그래서 RequestDto에 @NotNull 했던 것들을 Validation 해주는구나!
> @Valid는 MethodArgumentNotValidException 예외 발생
> @Validated는 ConstrainViolationException 예외를 발생시킴
- 예시
> Validator 적용 전
>> @ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
>> 성공: Controller 정상 호출
>> 실패: TypeMismatch FieldError 발생
> Validator 적용 후
>> @ModelAttribute -> 각 필드 바인딩 -> 성공한 필드만 Bean Validation 적용
>> Integer 타입 필드에 문자가 오면 애초에 검증의 의미가 없음
>> 성공: String 필드에 문자입력 -> 바인딩 성공 -> String 필드에 Bean Validation 적용
>> 실패: Integer 필드에 문자입력 -> 바인딩 실패 -> bindingResult에 TypeMismatch FieldError 추가 -> 바인딩에 실패한 필드는 값이 없음(null) -> Bean Validation 적용X
Bean Validation 2
- Spring의 Bean Validation은 Default로 제공하는 Message들이 존재하고, 임의로 수정할 수 있음
- Spring에서는 오류 메시지 코드관리를 위해 MessageCodeResolver 인터페이스의 구현체인 DefaultMessageCodesResolver를 기본으로 사용
- 에러 메시지 수정 방법
> 1. NotNull.Object.fildName ... 필드 위 @NotBlank(message = "메세지 수정 가능") 과 같은 방법으로 설정
> 2. NotNull.fieldName(MessageSource) ... 필드명에 맞춘 사용자 정의 Message
> 3. NotNull.FieldType(MessageSource) ... 필드 타입에 맞춘 사용자 정의 Message
> 4. NotNull ... Annotation 자체에 포함된 Default Message
> MessageSource란 Spring에서 지원하는 인터페이스. 메세지의 국제화를 위해 사용됨
> 2, 3번은 messages.properties에서 설정. 2번은 필드별 메세지 설정, 3번은 타입별로 공통 메시지 설정. 나중에 알아둘 필요가 있을 듯
- 필드 단위가 아닌 객체 전체에 대한 오류를 나타냄. 예를 들어 두 필드간의 관계를 검증할 때 ObjectError를 통해 해당 오류를 BindingResult에 기록할 수 있음
- @ScriptAssert... 예시: 비밀번호/비밀번호 확인 필드 동일 검증.
> 실제로는 제약사항 때문에 사용X / 실무에서는 훨씬 복잡한 Validation들이 필요하지만 대응이 불가
> Java코드로 직접 Validation 하는 것을 권장
- Java 코드로 구현하기 ... 예) 요구사항: 총 구매 가격이 10000원 이상
> price * count >=10000

- 이렇게 Object Error는 로직으로 구현
- @ModelAttribute는 기본 생성자만 있을 때는 setter를 통해 파라미터를 바인딩하고 다른 매개변수를 가진 생성자가 있다면 그 생성자를 이용해 파라미터 바인딩을 수행
Bean Validation 3
- 등록, 수정 API에서 각각 다른 Validation이 적용된다면?
- 예시 : 상품(id(식별자), name(이름), price(가격), count(재고)) 가 있다고 할 때
> 상품 등록 API에서는 1) 식별자 값 필수X / 2) name은 null, "", " "허용X / 3) price는 10~10000 사이 숫자로 생성 / 4) count는 1~999 사이의 숫자로 생성
> 상품 수정 API에서는 1) 식별자 값 필수 / 2) name은 null, "", " " 허용X / 3) price는 무제한으로 허용 / 4) count는 1~999 사이의 숫자로 생성
- 해결 방법
> 1. 저장할 Object를 직접 사용하지 않고 SaveRequestDto, UpdateRequestDto 따로 사용
> 2. Bean Validation의 groups 기능 사용
- Bean Validation의 groups 속성은 다양한 유효성 검사 시나리오를 정의할 때 사용. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 groups를 활용할 수 있음
- interface를 만들고, FieldError Validation Annotation에서 (groups={SaveCheck.class, UpdateCheck.class})와 같이 할 수 있음
- Bean Validation의 충돌이 발생하는 경우 대부분 DTO를 분리하는 방법이 적절
- groups VS DTO 분리
> groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용 -> 가독성이 떨어지고 코드 복잡도가 올라감
> 실무에서는 등록 폼과 수정 폼 자체를 분리해서 사용하기 때문에 DTO 분리 방법을 사용하면 됨
> 단, 네이밍은 일관성 있게 작성해야 함(SaveRequestDto, UpdateRequestDto)
- DTO 분리
> 실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있음
> 각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있음
> RequestDto가 변한다는 건 해당 API의 스펙 자체가 변경되어 많은 수정이 발생
> 실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하는게 좋음
- @Validated VS @Valid
> 1. @Validated : 속성값이 존재 / Spring이 제공하는 Annotation
> 2. @Valid : 속성값이 미존재 / groups 지원X / groups 기능을 사용하려면 @Validated를 사용해야 함 / Java 표준 Annotaion
Bean Validation 4
- @ModelAttribute, @RequestBody
- @Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용할 수 있음.
- @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용
- @RequestBody는 HTTP Body Data를 Object로 변환할 때 사용
- @RestAPI 요청의 세가지 경우의 수
> 1. 성공 요청: 성공 > Controller 정상 호출 / 응답 반환
> 2. 실패 요청: JSON을 객체로 변환하는 것 자체가 실패 > Controller 미호출
> 3. 검증 오류 요청: JSON을 객체로 변환하는 것은 성공, 검증에서 실패 > bindinResult.getAllErrors()가 MessageConverter에 의해 JSON으로 변환되어 반환
>> Controller 실제 호출
>> bindingResult error들이 콘솔에 출력됨
- @ModelAttribute와 @RequestBody 차이점
- 1. @ModelAttribute
> 각각의 필드 단위로 바인딩
> 특정 필드 바인딩이 실패해도 나머지 필드는 정상적으로 검증 처리
> 특정필드 변환 실패 > 컨트롤러 호출 / 나머지 필드 Validation 적용
- 2. @RequestBody
> 필드별 적용X 객체 단위로 적용
> MessageConverter가 정상적으로 동작하여 Object로 변환하여야 Validation이 동작
> 특정필드 변환 실패 > 컨트롤러 미호출, Validation 미적용
- 추가 내용
> bindingResult.getAllErrors()는 FieldError와 ObjectError 모두 반환
> Spring은 MessageConverter를 이용해 Error 객체들을 변환하여 응답
> RequestDTO의 경우 생성, 수정, 삭제 모두 비슷하게 생겼어도 따로 분리할 것
> 강의에서 작성한 코드는 예시이고, 실제로는 API Spec에 맞는 응답을 만들어 클라이언트에 전달해야 함 / @ControllerAdvice
마무리: 이것만은 꼭 기억하기
- Bean 관리, 의존성 주입
- Spring Container과 관리하는 Java 객체
- 제어의 역전(IOC) : 객체 생성과 의존성 관리를 개발자가 아닌 Spring이 담당
- 의존성 주입(DI) : 객체 간의 의존성을 Spring Container가 주입
- Spring Bean은 기본적으로 싱글톤 패턴으로 관리되어 하나의 인스턴스만 생성 / Stateless
- 자동등록
> @ComponentScan(Spring Boot) + @Component(@Controller, @Service, @Repository)
- 수동 등록
> @Configuration @Bean
- 생성자 주입
> 생성자 + Autowired를 통해 의존성을 주입받음. 불변, 실수 방지
- @RequiredArgsConstructor
> final 필드에 자동으로 생성자를 생성해 주입
- BindingResult
> Validation 오류 정보를 담는 객체
- Bean Validatin
> Annotation 기반으로 Validation이 가능하도록 만들어진 표준 기술(예, @NotNull @NotBlank 등)