반응형

RDBMS

1. Oracle과 Mysql 비교

  Oracle Mysql
구조 DB 서버가 통합된 하나의 스토리지를 공유 DB서버 마다 독립적인 스토리지를 할당
조인방식 nested loop join, hash join, sort merge join 제공 nested loop join 제공
확장성 별도의 DBMS 설치해 사용할 수 없음 별도의 DBMS를 설치해 사용할 수 있음
메모리 메모리 사용율이 커서 최소 수백 MB이상이 되어 설치가능 메모리 사용율이 낮아서 1MB 환경에서도 설치 가능
구문이 상이함 (null 체크, 현재 시간, 페이징 처리등)

 

2. Index

2.1 인덱스란?

정의  쉽게 찾아 볼수 있도록 일정한 순서에 따라 놓은 목록
목적  원하는 값을 빠르게 찾는것이 목적(select 하기 위함, where절에 사용해야 의미가 있음)
특징  항상 최신 상태를 유지하고, 인덱스도 하나의 데이터베이스 객체로 약 10%정도 저장용량이 필요함
 인덱스를 저장할때 B-TREE 자료 구조를 사용함

※ B-Tree 자료구조

 데이터를 추가 할때 인덱스가 있을 경우 페이지 분할이 발생 할 수 있어 속도 이슈가 생길 수 있음.

 

2.2 인덱스 적용 기준

카디널리티가 높은(중복도가 낮은) 컬럼

where, join, order by 절에 자주 사용되는 컬럼

insert, update,delete가 자주 발생하지 않는 컬럼

규모가 작지 않은 테이블

 

2.3 클러스터링 인덱스

primary key나 not null과 unique로 지정할 수 있음(primary key가 우선순위가 높음)

실제 데이터 자체가 저장되고 테이블당 1개만 존재가 가능

 

2.4 논클러스터링 인덱스(보조 인덱스, 세컨더리 인덱스)

unique나 unique index, index로 지정이 가능

별도의 인덱스 페이지를 생성하고 테이블당 여러개가 존재 가능

※ 실제 데이터 페이지는 그대로 두고 리프페이지에서 실제 데이터 페이지 주소를 담고 있음

 

2.5 다수의 인덱스

※ 논클러스터링 인덱스로 쿼리 조회 과정

 실제 논클러스터링 인덱스 페이지에선 데이터페이지 주소가 아닌 클러스터링된 컬럼 값을 가지고 있어서 그 컬럼값으로 클러스터링 인덱스 페이지에서 루트페이지를 통해 리프페이지에서 조회함

 데이터 추가시 페이지 분할이 일어나면 많은 논클러스터링 데이터 페이지주소가 변경 될 수 있기 때문에 페이지주소가 아닌 컬럼 값을 가지고 있음

 

3. Join의 수행 원리 3가지

3.1 Nested Loop Join

2중 for문과 비슷

1:N의 관계일 때 1에 해당되는 소량의 데이터를 가진 테이블을 outer table로 설정, N에 해당되는 대량의 데이터를 가진 테이블을 inner table로 설정 해야 조회 속도가 빠름

인덱스에 의한 랜덤 엑세스를 기반으로 하고 있기 때문에 대량 데이터 처리시 불리함

outer table에 조인을 위한 적절한 인덱스가 생성되어있어야함

다수의 트렌젝션을 처리하는 온라인 트렌잭션인 OLTP성 환경에 적합

※ outer table = driving 테이블, inner table = driven table

 

3.2 Sort Merge Join

2중 for문이지만 join 컬럼 기준으로 sorting 후 join 시킴

join에 적절한 인덱스가 없을 경우 사용

대용량 자료를 join할때 사용

Equal join이 아닌 범위로 join 할 경우 사용(범위 join)

인덱스 사용에 따른 랜덤 엑세스의 오버헤드가 많은 경우 사용

 

3.3 Hash Join

• inner table이 대용량일 때 outer join을 bulid input으로 삼아서 hash영역에 저장

※ hash 영역은 PGA(메모리)영역이기 때문에 처리속도가 빠름

  Hash영역으로 올라갈때 join 컬럼을 기준으로 Hash function이 적용 되기 때문에 key 컬럼에 중복값이 적을 수록 유리

배치에서 쓰면 좋은 수행원리, 대용량 table join 시 유리

Hash 영역에 들어간 테이블 사이즈가 너무 크면 PGA영역의 메모리를 넘어 디스크영역을 사용하기 때문에 성능에 불리

반응형
반응형

SPRING

1. 스프링이란

 자바의 오픈 소스 애플리케이션 프레임워크중 하나로 스프링의 기본 철학은 특정  기술에 종속 되지 않고 객체를 관리할 수있는 프레임워크를 제공하는것

컨테이너로 자바객체를 관리하면서 의존성 주입과 제어의 역전을 통해 결합도를 낮춤

 

2. DI

2.1 DI란

Dependency Injection 의존성 주입

객체간 의존관계를 미리 설정해두면 스프링 컨테이너가 의존 관계를 자동으로 연결해줌

컴포넌트 스캔을 통해 하기의 어노테이션이 붙어있으면 자동으로 스프링 빈에 등록해줌 

- @Component : 컴포넌트 스캔에서 사용

- @Controlller : 스프링 MVC 컨트롤러에서 사용

- @Service : 스프링 비즈니스 로직에서 사용

- @Repository : 스프링 데이터 접근 계층에서 사용

- @Configuration : 스프링 설정 정보에서 사용

생성자 주입 시 @Autowired를 쓰거나, 롬복의 @RequireArgsConstructor를 사용

 

2.2 DI 하는 방법 및 장단점

DI 방법 장점 단점
필드 주입  코드가 간결  변경 불가
 프레임워크에 의존적
 테스트 코드 작성시 객체수정이 불가
setter 주입  객체 생성 이후에도 객체 변경가능
 선택적으로 생성해야하는 객체나 변경이 발생하는 의존관계에서 사용
 public으로 구현하기 때문에, 관계를 주입 받는 객체의 변형 가능성을 열어둠
생성자 주입  생성자의 호출 시점에 1회 호출 되는 것이 보장
 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해
 

 

3. IoC (Spring 3대요소 중 하나)

Inversion of Control 제어의 역전

제어권이 사용자에게 있지 않고 프레임워크에 있어서 필요에 따라 사용자의 코드를 호출함

스프링에서는 인스턴스의 생성부터 소멸까지 개발자가 아닌 컨테이너에서 대신 관리함

 

4. AOP (Spring 3대요소 중 하나)

4.1 설명

관점 지향 프로그래밍

여러 객체에 공통으로 적용 할 수 있는 기능을 분리하여 개발자는 반복작업을 줄이고 핵심 기능 개발에만 집중할 수 있음

Object Oriented Programming(객체지향 프로그래밍)을 보완 할 수 있는 패러다임

※ 여러 오브젝트에 나타나는 공통 부가기능을 모듈화하여 재사용

 

4.2구현 방법

※어떤 부가기능을 어디에 적용시킬지 파악

Advice 

- Joinpoint에서 실행되어야 하는 프로그램 코드

- 독립된 클래스의 메소드로 작성
- 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
- 관심사를 구현한 소스코드

- 종류 5가지

@Before : 메서드가 실행되기전에 사용되는 Advice

@AfterReturning: 메서드가 정상적으로 실행되었을 때 사용되는 Advice

@AfterThrowing : 메서드가 예외를 발생 시켰을 때 사용되는 Advice

@After : 메서드가 정상적으로 실해되거나, 예외를 발생시켰을때 사용되는 Advice

@Around : 비지니스 로직 전후 실행되는 Advice

 

 JoinPoint

- Advice가 적용 될 수 있는 위치, 어플리케이션 실행 흐름에서의 특정 포인트(AOP를 적용할 수 있는 지점)를 의미

- 메소드를 호출하는 '시점', 예외가 발생하는 '시점'과 같이 애플리케이션을 실행할 때 특정 작업이 실행되는 '시점'을 의미
- Advice를 적용할 수 있는 후보 지점 혹은 호출 이벤트
- Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
- 관심사를 구현한 코드를 끼워 넣을 수 있는 프로그램의 이벤트를 말하며, 예로는 call events, execution events, initialization events 등이 있음

 

PointCut

- joinpoint의 상세한 스펙을 정의한것, 언제 Advice를 실행할지를 정의
- Target 클래스와 Advice가 결합(Weaving)될 때 둘 사이의 결합규칙을 정의
- 예로 Advice가 실행된 Target의 특정 메소드 등을 지정
- JoinPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음
- 관심사가 주프로그램의 어디에 횡단될 것인지를 지정하는 문장이며, 에로는 before call(public void update*(...))등이 있음

 

5. PSA (Spring 3대요소 중 하나)

Portable Service Abstraction. 필요에 따라 바꿔 끼울 수 있는 서비스 추상화

 Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것

 

6. servlet Filter, Spring Interceptor, AOP의 차이

6.1 호출 순서

 Filter → Interceptor → AOP → Controller → AOP → Interceptor → Filter

 

6.2 Filter

요청과 응답을 거른뒤 정제하는 역할

Dispatcher Servlet 이전에 실행

 지정된 자원의 앞단에서 요청 내용을 변경하거나, 여러가지 체크를 수행

자원 처리가 끝난 후 응답 내용에서도 변경 처리 가능

일반적으로 인코딩 변환, XSS 방어등 요청에 대한 처리로 사용

Init() : 필터인스턴스 초기화

 doFilter() : 전/후 처리

 distroy() : 필터인스턴스 종료

 

6.3 Interceptor

요청에 대한 작업 전/후로 가로챔(Dispatcher Servlet, controller 호출 전/후)

스프링 컨텍스트(context 영역) 내부에서 Controller(Handler)에 관한 요청과 응답에 대해 처리

스프링의 모든 빈 객체에서 접근 가능

 인터셉터 여러개 사용가능(로그인, 권한, 로그 등)

preHandler() : 컨트롤러 메서드 실행전

postHandler() : 컨트롤러 메서드 실행후 view 페이지 렌더링 전

afterCompletion() : view 페이지가 렌더링 된 후

 

6.4 스프링 프레임워크 동작 순서 

 

HTTP 요청 → 핸들러 조회  핸들러 어댑터 조회  핸들러 어댑터 실행  핸들러 실행  ModelAndView 반환 viewResolver 호출  View 반환  뷰 렌더링 HTML 응답

 

※ 스프링 프레임워크 단순 버전

HTTP 요청 → DispatcherServlet에서 매핑할 컨트롤러가 있는지 HandlerMapping에서 조회 → controller 실행 → 요청 처리 후 결과를 출력한 view 이름을 DispatcherSerlvet에 리턴 → 컨트롤러에 보내온 View이름을 ViewResolver에게 전달 → 처리결과를 View에 송신 → Dispatcher서블릿에서 클라이언트에게 최종 결과 출력

 

7. @Bean이 무엇인지, new 연산자를 통해 오브젝트를 생성하는것과 무슨 차이가 있는지

 @Bean

 - 스프링 컨테이너가 관리하는 자바 객체

 - Bean의 등록 방법

   : @Component 에노테이션을 사용하면 컴포넌트 스캔이 자동으로 이루어짐

   : @Configuration에 직접 @Bean 어노테이션 사용

• new 연산자와 @Bean을 주입 받는것의 차이점

 - new 연산자는 실행되는 시점에 인스턴스를 생성

 - @Bean은 @Bean에 등록된 이미 존재하는 인스턴스를 프로그램에 맞도록 주입 시켜줌

 - @Bean 등록 시 싱글톤으로 관리되어 같은 객체 주소를 참조함

※ 클래스간 결합도를 낮추고 응집도를 높임

 

8. 싱글톤

8.1 싱클톤이란

 객체의 인스턴스가 오직 한개만 생성되는 디자인 패턴

메모리낭비 방지, 속도가 빠름, 데이터 공유가 쉬움

 

8.2 싱글톤의 단점

싱글톤인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시키면 다른 클래스간의 결합도가 높아짐

개방 폐쇄 원칙에 위배(확장에는 열려있고 수정에는 닫혀있는 원칙)

 

8.3 스프링에서 싱글톤

스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리

객체 인스턴스를 공유하기 때문에 객체 상태를 유지하게 설계하면 안됨

 

9. Spring과 Spring Boot의 차이점

  스프링 프레임워크 스프링 부트
Dependency  dependency를 설정해줄 때 설정 파일이 매우 길고, 모든 dependency에 대해 버전 관리도 하나하나 해줘야 함  dependency를 Spring Framework보다 쉽게 설정해 줄 수 있고, 라이브러리간 버전 관리도 자동으로 해줌
AutoConfiguration configuration설정을 할 때 매우 길고, 모든 어노테이션 및 빈 등록 등을 설정해 줘야함 application.properties파일이나 application.yml파일에 설정하면 됨
편리한 배포 war파일을 Web Application Server에 담아 배포 Tomcat 이나 Jetty 같은 내장 WAS를 가지고 있기 때문에 jar 파일로 간편하게 배포

※ 스프링부트의 단점: 대부분 만들어져있고 확장이 가능하기 때문에 소스를 분석하거나 로직을 이해할 때 깊숙히 파고들어야함

 

 

10. Spring data JPA

10.1 영속성 컨텍스트란?
 객체지향 적인 코딩이 가능
 컬럼 추가시 쿼리를 작성할 필요없이 필드만 추가하면 됨
 특징 : 영속성 컨텍스트라는 것이 존재함으로 인해 발생하는 특징들이 생김
 같은 키로 조회하면 항상 동일한 오브젝트가 리턴되는것이 보장됨
 더티체킹, 엔티티의 변경된 부분만 찾아서 업데이트 함
 지연된 쓰기 지연, Create, Update, Delete시 쿼리를 캐시에 저장하고 있다가 한번에 처리함


10.2 N+1 문제가 무엇인가? 어떻게 해결할 수 있는가?

 

10.3 단방향 연관관계, 양방향연관관계를 설정해야 하는 경우는?


10.4 양방향연관관계에서 연관관계의 주인은 어디에 설정해야하나?


10.5 영속성전이(Cascade)에 대하여


10.6 연관관계가 없는 컬럼끼리 조인을 해야 하는 경우가 있음


10.7 연관관계가 복잡한 쿼리는 JPA로 힘든 부분이 있을것 같은데 어떻게 처리 했는지

 

 

 

반응형
반응형

JAVA

1. 기본형 변수 타입

  1 byte 2 byte 4 byte 8 byte
논리형 boolean      
문자형   char    
정수형 byte short int long
실수형     float double

※ 1 byte = 8 bit

 

2. 인자(argument)와 매개변수(parameter)

 호출할때 "인자"를 전달 한다고 표현

 메서드를 만들때 "매개변수"를 지정했다고 표현

 

3. 오버로딩 VS 오버라이딩

 오버로딩 : 기존에 없던 새로운 메서드를 정의 하는것 (같은 메서드명, 다른 매개변수)

 오버라이딩 : 상속 받은 메서드의 내용을 변경하는것(메서드 재정의)

 

4. 추상클래스(abstract) VS 인터페이스(interface)

 추상 클래스 : 추상메서드가 하나이상 포함되어있어야함, 상속되어 구현되는것이 목적

 인터페이스 : 모든 메서드가 추상 메서드로 정의 되어있어야 함, 인터페이스를 구현한 객체들은 같은 메서드가 있다는 보장

 

5. 자바 에플리케이션 실행 과정

 자바 소스 코드 : 개발자가 Java로 작성한 소스 파일로 확장자 명은 .java

 자바 바이트 코드 : 클래스코드가 읽을 수 있도록 컴파일된 파일 확장자명은 .class

 Methode Area : 클래스가 사용되면 클래스별로 클래스 정보가 저장되는 영역(static 변수, static 메소드 등)

 Heap Area : 인스턴스가 생성되는 영역 (Garbage Collection의 대상이 되는 영역)

 Stack Area : 메서드 실행에 필요한 메모리 공간(매개변수, 지역변수, 리턴정보등 을 저장)

 PC Register : 현재 수행중인 JVM 명령어가 저장

 Native Method Area : 자바 외의 언어 (C, C++ 등)를 수행하기위한 영역

 

6. 가비지 컬렉션(Garbage Collection)

JVM에서 메모리를 관리해주는 모듈

Heap 메모리를 재활용 하기 위해 더이상 참조하지 않는 인스턴스들을 메모리에서 제거

개발자가 직접 메모리를 정리하지 않아도 됨

참조 되지 않는 객체를 찾는 과정에서 Mark and Sweep이 발생하여 스레드가 잠깐 멈춤

 

 

7. 객체 지향 프로그래밍

 현실 세계의 사물과 같은 객체를 만들고, 객체에 필요한 특징을 뽑아 프로그래밍을 수행

 특징

    - 추상화 : 객체들의 공통적인 특징(기능, 속성)을 도출하는 것

    - 캡슐화 : 실제 구현되는 부분을 외부에 드러나지 않도록 은닉

                    객체가 독립적으로 역할을 수행하도록 데이터를 하나로 묶어 관리 (응집도가 높아짐)

                    데이터를 보이지 않고 외부와 상호작용을 할 때 메서드를 이용 (결합도가 낮아짐)

    - 상속성 : 하나의 클래스가 가진 특징(함수, 데이터)을 다른 클래스가 그대로 물려받는것

    - 다형성 : 약간 다른 방법으로 동작하는 함수를 동일한 이름으로 호출하는것 (오버로딩, 오버라이딩)

 

 좋은 객체 지향 설계의 5가지 원칙(SOLID)
    - SRP 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야한다.
변경이 있을때 파급효과가 적으면 단일 책임 원칙을 잘 따른것 (객체의 생성과 사용을 분리)
※ 책임의 범위를 잘 조절해야함

    - OCP 개방 폐쇄 원칙 : 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다. (다형성)
역할과 구현을 분리, 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하는것.

    - LSP 리스코프 치환 원칙 : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 함. 컴파일은 잘 되더라도 규약을 위배하면 LSP 위반.
자동차 인터페이스의 엑셀은 앞으로가라는 기능, 뒤로가게 구현하게되면 LSP 위반, 느리더라도 앞으로가야함.

    - ISP 인터페이스 분리 원칙 : 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
인터페이스가 명확해지고, 대체 가능성이 높아짐

    - DIP 의존 관계 역전 원칙 : 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
스프링의 의존성 주입은 이 원칙을 따르는 방법중에 하나.
클라이언트 코드가 구현 클래스를 바라보지 말고 인터페이스를 바라보라는 뜻.
역할에 의존해야지 구현에 의존하지 말라는것.
의존 = 내가 그 코드를 안다.

 

 

8. 컬렉션 프레임 워크

8.2 Java 컬렉션 프레임워크 구조도

데이터 군을 저장하는 클래스를 표준화 한 설계

List  순서가 있는 데이터 집합, 데이터의 중복을 허용함
 ArrayList, LinkedList, Stack, Vector
Set • 순서를 유지 하지 않는 데이터 집합, 데이터 중복을 허용하지 않음
HashSet, TreeSet
Map 키와 값의 쌍으로 이루어진 데이터 집합, 순서유지가 안됨, 키는 중복이 불가, 값은 중복이 가능
HashMap, TreeMap, HashTable

8.2 List

순서가 있고 중복을 허용, 인덱스로 원소에 접근이 가능, 크기가 가변적

ArrayList

      ◦ 배열을 기반으로 데이터를 저장

      ◦ 단반향 포인터 구조로 데이터 순차적 접근에 강점

      ◦ 데이터 삽입, 삭제에 불리하나, 순차적 추가/삭제는 빠름

      ◦ 임의의 요소에 대한 접근성이 뛰어남

 LinkedList

      ◦ 연결 기반으로 데이터를 저장

      ◦ 각 요소(node)들은 자신과 연결된 다음요소에 대한 참조 주소값과 데이터로 구성되어있음

      ◦ 양방향 포인터 구조로 데이터 삽입, 삭제가 빠름

       임의의 요소에 대한 접근성이 안좋음

 

8.3 Set

 순서가 없고 중복된 데이터를 허용하지 않음, 중복되지 않은 데이터를 구할 때 유용, 빠른 검색 속도를 가짐

HashSet

      ◦ 인스턴스의 해시값을 기준으로 저장하기 때문에 순서를 보장하지 않음

      ◦ NULL 값을 허용
      ◦ TreeSet보다 삽입, 삭제가 빠름

LinkedHashSet 

      ◦ 입력된 순서를 보장

 TreeSet

      ◦ 이진 탐색 트리(Red-Black Tree)를 기반으로 함
      ◦ 데이터들이 오름차순으로 정렬
      ◦ 데이터 삽입, 삭제에는 시간이 걸리지만 검색, 정렬이 빠름

 

8.4 Map

Key와 Value의 한쌍으로 이루어지는 데이터의 집합. Key에 대한 중복이 없으며 순서를 보장하지 않음, 뛰어난 검색 속도

 HashMap
      ◦ Key에 대한 중복이 없으며 순서를 보장하지 않음
      ◦ Key와 Value 값으로 NULL을 허용
      ◦ 동기화가 보장되지 않음
      ◦ 검색에 가장 뛰어난 성능을 가짐
 HashTable
      ◦ 동기화가 보장되어 병렬 프로그래밍이 가능하고 HashMap 보다 처리속도가 느림
      ◦ Key와 Value 값으로 NULL을 허용하지 않음
 LinkedHashMap
      ◦ 입력된 순서를 보장
 TreeMap
      ◦ 이진 탐색 트리(Red-Black Tree)를 기반으로 키와 값을 저장
      ◦ Key 값을 기준으로 오름차순 정렬되고 빠른 검색이 가능
      ◦ 저장 시 정렬을 하기 때문에 시간이 다소 오래 걸림

 

9. HashTable

※ 해시함수(key) → 해시코드 Index   Value

검색하고자하는 key 값을 입력 받아 해시함수로 로직을 수행

반환된 해시코드를 배열의 Index로 환산하여 데이터에 접근 하는 자료구조

 

10. String, StringBuffer, StringBuilder

10.1 String VS StringBuffer, StringBuilder

String : 불변의 속성을 가짐

// ① 메모리 할당
String str = new String("hello");

// ② 기존 메모리가 아닌 새로운 메모리에 할당
str = str+" world";

 

 StringBuffer, StringBuilder

// ① 메모리할당
StringBuffer sb = new StringBuffer("hello");

// ② 기존 메모리에 append
sb.append(" world");

 

10.2 StringBuffer VS StringBuilder

StringBuffer : 동기화 키워드를 지원하여 멀티스레드 환경에서 안전함

StringBuilder : 동기화 지원이 안되지만 단일스레드 환경에서 성능이 좋음

 

11. 제네릭스(Generics)

 다양한 타입의 객체들을 다루는 메서드나 컬렉션에 컴파일 시 타입을 체크해주는 기능(JDK 1.5에 도입)

 타입의 안정성 제공

 타입체크와 형변환을 생략할 수 있어 코드가 간결해짐

 

12. CheckedException과 UnCheckedException

  CheckedException UnCheckedException
처리여부 개발 시 반드시 예외를 처리해야함 예외 처리를 강제 하지 않음
확인시점 컴파일 단계 실행단계(Run Time)
예외 종류 RunTimeExcepion을 제외한 모든 예외
SQLException
IoException
RunTimeException 하위 예외
NullPointException
IndexOutOfBoundException

 

13. int와 double, Integer와 Double등의 래퍼 타입과 Primitive 타입의 차이점

 Primitive 타입은 변수에 값자체를 저장하지만 래퍼타입은 변수에 객체의 주소 값을 저장

int에는 null을 넣을수 없지만 Integer는 객체이기 때문에 null 입력이 가능

Boxing : Primitive 타입을 래퍼타입으로 바꾸는것

UnBoxing : 래퍼타입을 Primitive 타입으로 바꾸는것

 

14. 익명 클래스와 람다 표현식의 차이

 익명 내부 클래스는 새로운 클래스를 생성하지만, 람다는 새로운 메서드를 생성하여 포함.
 내부 클래스는 새로운 클래스파일이 생성
 람다는 static 이든, 객체 사용을 위한 non-static 이든, 메서드로 생성.
익명 내부 클래스의 this : 새로 생성된 클래스, 람다의 this : 람다식을 포함하는 클래스

람다표현식이 클래스 정의와 구현을 동시에 하여 코드가 더 간결함

프로그램 내에서 한번 만 객체로 만드는데 클래스를 정의하고 생성하는 것이 비효율적 

 

15. 스레드를 생성하는 방법

 Runnable 인터페이스를 Implements 하여 run 메서드를 정의

Thread 클래스를 상속 받아 run 메서드를 오버라이딩

 

 

JSP

1. Servlet과 JSP, MVC

Servlet

 - 자바 소스코드에서 response로 PrintWriter객체에 HTML 소스를 삽입하여 response를 함

 - 웹페이지를 동적으로 생성하기 위한 서버측 프로그램

 - 자바를 기반으로 만들어지면 WAS위에서 컴파일되고 동작함

 

JSP

 - JavaServerPage의 약자로 HTML 소스에 스클립틀릿(<% ... %>)에 자바소스를 작성

 - 웹페이지를 동적으로 생성하기 위한 웹 어플리케이션 도구

 - Java 기반의 서버사이드 스크립트 언어

 ※ 스크립트 언어란 컴파일 없이 내장된 번역기로 실행할 수 있는 프로그래밍언어

 - JSP 컨테이너에서 서블릿을 생성하여 컴파일 후 실행하는 구조

 

MVC

 - Model View Controller : 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것

 - 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행.  뷰에 전달할 결과 데이터를 조회해서 모델에 담음.

 - 모델: 뷰에 출력할 데이터를 담아서 뷰에 전달함. 따라서 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있음

 - 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중. (HTML을 생성하는 부분)

 

 

2. EL과 JSTL

JSP 작성을 도와주는 언어와 라이브러리

EL

 - Expression Language

 - JSP에서 객체의 값을 쉽게 꺼내 쓸수 있는 도구

 - HTML : <span>${prod.no}</span>

 - Javascript : "${prod.no}"

 - 스크립틀릿안에서 값을 가져오고 할당할 필요가 없음

      <%=((Product)request.getAttribute("prod")).getNo()%>

 

JSTL

 - JSP Standard Tag Library

 - 일반적으로 JSTL + EL의 조합을 의미함

 - JSP에서 자주 사용하는 스클립틀릿을 하나의 태그로 묶은 라이브러리

 ex) <c:set>, <c:if>, <c:forEach> ...

 

※ <c:out> 태그를 쓰는 이유

- Cross-site Scripting 공격은 스크립트를 주입시켜서 사이트를 침입

- c:out 태그를 사용할 시에 이 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경해줌(escape)

- 크로스 사이트 스크립팅을 막아줌

반응형
반응형

프로젝트 생성 - STS 4.x 세팅

1. 개발 툴 설치

1.1 STS 4.x 설치 : https://spring.io/tools

 

Spring | Home

Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.

spring.io

 

1.2 H2 DB 설치 : https://www.h2database.com/html/download-archive.html

• 버전은 1.4.199 ZIP으로 다운로드

 

Archive Downloads

 

www.h2database.com

- H2 실행

zip파일 압축 해제 > h2경로의 bin 폴더 접근 > h2w.bat실행 > 브라우저에 창이 뜸 > localhost:8082로 입력

Generic H2 (Server) 선택 > JDBC URL에 jpa_study로 설정 > C:\Users\xxxxx에 jpa_study.mv.db명으로 빈 파일명 입력 > 연결 버튼 클릭

 

2. 프로젝트 세팅

2.1 신규 프로젝트 생성

• create a project 클릭 > Maven Project 선택 > Next 클릭

 

2.2 location 선택

• Next 클릭

 

2.3 maven 기본 틀 선택

• Filter에 org.apache.maven 검색 > artifact Id : maven-archetype-quickstart 선택 > next 클릭

 

2.4 프로젝트 세팅

• 내용 작성 > Finish 클릭

 

2.5 콘솔창에서 y 입력 후 엔터

 

2.6 maven 디팬던시 추가

• workspace\ex1-hello-jpa\pom.xml 열기

• 컴파일러 버전 수정 및 dependency 추가

javax.xml.bind 추가 이유 : JAVA11에서 에러 발생

 

plugin 추가

org.apache.maven.plugins 추가 이유 : JAVA11에서 에러 발생

 

※ pom.xml 전체 소스

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>jpa-basic</groupId>
  <artifactId>ex1-hello-jpa</artifactId>
  <version>1.0.0</version>

  <name>ex1-hello-jpa</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
	<dependency>
		<groupId>javax.xml.bind</groupId>
		<artifactId>jaxb-api</artifactId>
		<version>2.3.0</version>
	</dependency>
	<!-- JPA 하이버네이트 -->
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-entitymanager</artifactId>
		<version>5.3.10.Final</version>
	</dependency>
	<!-- H2 데이터베이스 -->
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
		<version>1.4.199</version>
	</dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
            <source>8</source>
            <target>8</target>
          </configuration>
        </plugin>
		  
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

 

 

2.7 프로젝트 1.8로 세팅

• 프로젝트 우클릭 > properties 클릭 > Java Bulid Path 선택 > JRE System Library [JavaSE-1.7] 선택 > Edit 선택 > JavaSE-1.8(jre) 변경 후 Finish

 

 

2.8 JPA 세팅 정보 설정

• src/main/java 우클릭 > New > Package 클릭 > source folder에 java 삭제 후 name에 resources 입력 > Finish 클릭

• src/main/resources 우클릭 > New > Folder 클릭 > Folder name에 META-INF 입력 > Finish 클릭

• workspace\ex1-hello-jpa\src\main\resources\META-INF\persistence.xml 파일 생성

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
	xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
	<persistence-unit name="hello">
		<properties>
			<!-- 필수 속성 -->
			<property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
			<property name="javax.persistence.jdbc.user" value="sa" />
			<property name="javax.persistence.jdbc.password" value="" />
			<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpa_study" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
			
			<!-- 옵션 -->
			<property name="hibernate.show_sql" value="true" />
			<property name="hibernate.format_sql" value="true" />
			<property name="hibernate.use_sql_comments" value="true" />
			<!-- <property name="hibernate.hbm2ddl.auto" value="create" />-->
		</properties>
	</persistence-unit>
</persistence>

 

 

3.  실행 테스트

 App.java에 테스트 코드 작성

package jpa_basic.ex1_hello_jpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class App {
	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
		
		EntityManager em = emf.createEntityManager();
		
		em.close();
		emf.close();
	}
}

실행

 - App.java 우클릭 > Run > Java Application 클릭

 - 로그

반응형
반응형

JPA와 모던 자바 데이터 저장기술

1. 현재 개발 트렌드

애플리케이션 : 객체지향 언어 (ex: [Java, Scala, Kotlin...])

데이터베이스 : 관계형 데이터 (ex: [Oracle, Mysql, PostgreSQL...])

 객체를 관계형 DB에 관리하는 것에 시간을 많이씀

객체 데이터, 데이터 객체 : SQL 중심적인 개발이 됨

 

 

2. SQL 중심적인 개발의 문제점

2.1 기능추가, 테이블 생성 될때마다 CRUD SQL을 다 만들어야함

(JdbcTemplate, MyBatis가 Mapping에 도움을 주는 것은 있지만 그래도 한계가 있음)

 

회원 객체의 CRUD가 구현되있는 상황

 - 기존 회원 객체 테이블 기능 쿼리 구현

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
}

/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES ...
SELECT MEMBER_ID, NAME FROM MEMBER M
UPDATE MEMBER SET .

- 전화 번호 필드를 추가해야하는 상황

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
	private String tel;
}
/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES ...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ...TEL = ?

※ 모든 CRUD에 TEL을 하나하나 추가해야함

※ SQL에 의존적인 개발

 

 

2.2 패러다임의 불일치 (객체 VS RDBMS)

관계형 데이터베이스와 객체지향은 사상이 다름
객체지향개발 : 추상화, 캡슐화, 정보은닉, 상속, 다형성등 시스템의 복잡성을 제어하는 다양한 장치들 제공
관계형데이터베이스 : 데이터를 잘 정규화해서 저장하는 것이 목표.


 객체를 관계형 데이터베이스에 저장하는 도식

※ 객체를 SQL로 변환해서 RDB에 저장하는 변환과정을 개발자가 해야함

 

 

3. 객체와 관계형 데이터 베이스의 차이

3.1 상속

객체 상속관계 VS Table 슈퍼타입 서브타입 관계

 객체의 상속관계와 유사한 관계형 데이터베이스의 개념으로 Table 슈퍼타입, 서브타입 관계가 있음
상속 받은 객체(Album, Movie, Book)을 데이터베이스에 저장하려면 복잡함
 - 객체 분해 : Album객체를 Item과 분리

- Item Table에 하나, Album테이블에 하나 두개의 쿼리를 작성해서 저장

 Album 객체를 DB에서 조회하는것도 복잡
 - SQL로 ITEM과 ALBUM을 조인해서 데이터를 가져옴

 - 조회한 필드를 각각 맞는 객체(ITEM, ALBUM)에 매핑시켜서 가져와야함
결론: DB에 저장할 객체는 상속관계를 쓰지 않음

 

 

3.2 연관 관계

객체 연관 관계 VS 테이블 연관 관계

 객체는 참조를 사용 : member.getTeam();
 테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID


 Member와 Team간에 참조  
- 객체:  Member → Team 은 member.getTeam()을 통해 가능, Team → Member 는 참조할 객체가 없기 때문에 불가능
 - 테이블: 서로가 서로를 참조할 키(FK)가 있기 때문에 양측이 참조가 가능하다. Member ↔ Team


객체를 테이블에 맞춰 모델링, 테이블에 맞춘 객체 저장

/* 회원 객체 */
class Member{
	String id;        //MEMBER_ID 컬럼 사용
	Long teamId;      //참조로 연관관계를 맺는다.
	String username;  // USERNAME 컬럼 사용
}

/* 팀객체 */
class Team{
	Long id;      //TEAM_ID 컬럼 사용
	String name;  //NAME 컬럼 사용
}

/* 쿼리 */
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
INSERT INTO TEAM(TEAM_ID, NAME) VALUES...

- 객체 지향적이지 못함

 

 객체다운 모델링, 객체 모델링 저장

/* 회원 객체 */
class Member{
	String id;        // MEMBER_ID 컬럼 사용
	Team team;        // Team 객체 참조로 연관관계를 맺음
	String username;  // USERNAME 컬럼 사용
    
    Team getTeam(){
        return team;
    }
}

/* 팀객체 */
class Team{
	Long id;      // TEAM_ID 컬럼 사용
	String name;  // NAME 컬럼 사용
}


/* 쿼리에 teamId를 저장하기 위해 꺼냄*/
member.getTeam().getId();


/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ...
INSERT INTO TEAM(TEAM_ID, NAME) VALUES...

 

객체 모델링 조회

SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

public Member find(String memberId){
	//SQL 실행
	Member member = new Member();

	//데이터터베이스에서 조회한 회원 관련 정보를 모두 입력
	Team team = new Team();

	//회원과 팀 관계 설정
	member.setTeam(team);
	return member;
}

 

 객체 그래프 탐색

 - 객체는 자유롭게 객체 그래프를 탐색할 수 있어야 함

 - Member 객체에서 엔티티 그래프를 통해 Category 까지도 접근이 가능해야 함

ex) member.getOrder().getOrderItem().getItem().getCategory;

 

처음 실행하는 SQL에 따라 탐색 범위가 결정

SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

member.getTeam(); //OK
member.getOrder(); //null


class MemberService{
	...
	public void process(){
		Member member = memberDao.find(memberId);
		member.getTeam(); //????
		member.getOrder().getDelivery(); //????
	}
}

※ 엔티티 신뢰 문제가 발생

 - member안에 모든 참조를 자유롭게 참조 할 수 없음

 - 새로운 필드를 추가했는데 조회 로직에서 해당 부분 매핑을 빼놓을 가능성도 있음
 - 모든 코드와 쿼리를 분석해보기전까지는 엔티티 그래프 검색이 어디까지 되는지 확신할 수 없음 

 - 모든 객체를 미리 로딩할 수도 없음

memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam(); //Member와 Team조회
memberDAO.getMemberWithOrderWithDelivery(); // Member, ORder, Delivery 조회
...

 

※ 기존의 방식으로는 계층형 아키택처, 진정한 의미의 계층분할을 구현 할 수 없음

 

 

4. 객체 비교

동일한 식별자(memberId)로 조회한 두 객체 비교

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2; // false 다르다

class MemberDAO{
	public Member getMember(String memberId){
		String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
		...
		//JDBC API, SQL 실행
		return new Member(...);
	}
}

 - member를 조회할 때마다 new Member()를 통해 새로운 객체를 만들어서 조회 하기 때문에 두 인스턴스 비교는 내용물이 같더라도 참조값이 다름

 

• 자바 컬렉션에서 조회한 객체 비교

String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; // true 같다.

 

※ 동일성과 동등성(Identical & Equality)
 - 자바에서 두 개의 오브젝트 혹은 값이 같은가
: 동일한 오브젝트라는 말(identical)은 같은 레퍼런스를 바라보고 있는 객체라는 말로 실제론 하나의 오브젝트라는 의미

: 동등한 오브젝트라는 말(Equivalent)은 같은 내용을 담고 있는 객체라는 의미 

 

 

 

 

※ 결론

객체지향적으로 모델링을 할 수록 매핑작업만 늘어나고 불일치가 늘어나서 사이드이펙트가 커지기만함. 
객체를 자바 컬렉션에 저장하듯이 DB에 저장하기위해 나온 것이 JPA

 

 

JPA (Java Persistence API)

1. 용어

 JPA : 자바진영의 ORM 기술 표준

 ORM

- Object-relational mapping(객체 관계 매핑)
- 객체는 객체대로 설계 하고, RDBMS는 RDBMS대로 설계해서 ORM 프레임워크가 중간에서 매핑 해줌
- 대중적인 언어에는 대부분 ORM 기술이 존재

 

2. JPA 동작 원리

2.1 애플리케이션과 JDBC 사이에서 동작함

 

2.2 저장

 

2.3 조회

 

3. JPA를 사용하는 이유

3.1 생산성

• 저장: jpa.persist(member)
• 조회: Member member = jpa.find(memberId)
• 수정: member.setName(“변경할 이름”)
• 삭제: jpa.remove(member)

※ CRUD가 간편함

 

3.2 유지보수

/*회원 객체*/
public class Member {
	private String memberId;
	private String name;
	private String tel;
}
/*쿼리*/
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES ...
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET ...TEL = ?

※ 기존에는 필드 변경시 모든 SQL을 수정했어야했으나, JPA는 필드만 추가하면 되고 SQL은 JPA가 수행함

 

 

3.3 패러다임 불일치 해결

• 상속

 - 상속되어 있는 객체의 저장

 : 개발자의 할일  jpa.persist(album);

 : JPA가 나머지 처리

 

 - 상속되어 있는 객체의 조회

 : 개발자가 할일 Album album = jpa.find(Album.class, albumId);

 : JPA가 나머지 처리

 

 

• 연관관계

 - 연관관계 저장

member.setTeam(team);
jpa.persist(member);

 

• 객체 그래프 탐색

Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam()

※ JPA로 계층 엔티티를 신뢰할수 있음

 

• 비교

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다

※ 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장

 

 

3.4 성능 최적화 기능

 1차 캐시와 동일성 보장

String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2) //true

- 같은 트렌젝션 안에는 같은 엔티티를 반환(조회성능 향상)

- DB Isolation Level이 Read Committed이어도 애플리케이션에서 Repeatable Read 보장

- SQL을 한번만 수행함

 

※ DB Isolation Level(DB 격리 수준)

<아래로 내려갈수록 트랜잭션간 고립 정도가 높아지고 성능이 떨어짐>

 - READ UNCOMMITTED : 어떤 트랜잭션의 변경내용이 COMMIT이나 ROLLBACK과 상관없이 다른 트랜잭션에서 보여짐

 - READ COMMITTED : 어떤 트랜잭션의 변경 내용이 COMMIT 되어야만 다른 트랜잭션에서 조회할 수 있음

 - REPEATABLE READ : 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준

 - SERIALIZABLE : 읽기 작업에도 공유 잠금 설정, 동시에 다른 트랜잭션에서 이 레코드를 변경하지 못하게 됨

 

 

쓰기 지연

- insert

transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋

※ 트랜잭션을 커밋할 때까지 INSERT SQL을 모으고 JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송

 

- update

transaction.begin(); // [트랜잭션] 시작

changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않는다.

//커밋하는 순간 데이터베이스에 UPDATE, DELETE SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

※ UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
※ 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋

 

 

지연 로딩

- 지연 로딩: 객체가 실제 사용될 때 로딩
- 즉시 로딩: JOIN SQL로 한번에 연관된 객체까지 미리 조회

반응형
반응형

OAuth 소개

1. 우리서비스를 이용하는 USER의 니즈

• 우리의 서비스를 이용하는 USER가 우리 서비스에서 일정을 관리하면 USER가 가입한 그들의 서비스에도 공유가 되는 기능이 필요함.

 

2. 구현 방법

• 우리의 서비스는 USER가 사용하고 있는 그들의 서비스에 접근할수 있도록 허가를 받고 그 서비스에 접근하여 기능을 수행.

• 우리의 서비스가 그들의 서비스에 접근하도록 허가를 받는 가장 간단한 방법

- USER가 그들의 서비스의 계정정보(ID,PW)를 우리 서비스에게 전달해서 그들의 서비스에게 허가 받음

하지만 우리의 서비스도, USER도, 그들의 서비스도 모두 불편....

 

3. OAuth가 하는일

• 우리의 서비스와 USER가 사용하고 있는 그들의 서비스의 안전한 상호작용을 가능하게 해줌

• USER의 계정 정보를 우리의 서비스에게 넘기는게 아니라 USER의 요청에 의해 그들의 서비스가 AccessToken이라는 일종의 비밀번호를 발급 (나의 서비스가 필요한 부분만 허용하는 비밀번호)

• 우리의 서비스가 OAuth를 통해 AccessToken을 획득한 후 AccessToken을 통해서 그들의 서비스에 접근 하여 데이터를 가져오고 수정하고 생성하고 삭제할 수 있음.

• 이걸 가능하게 해주는 기능이 OAuth

• USER의 계정 정보를 우리의 서비스에서 처음부터 보관하지 않고 회원을 식별할수 있는 기능을 구현할 수 있음

(USER가 가입한 그들의 계정을 통해 우리의 서비스에서 로그인 기능을 연동, federated identity라고함)

※ 이 기술의 기반 기술이 OAuth

 

 

OAuth의 3개의 주체

 USER = Resource Owner

 Mine(우리의 서비스) = Client

 Their(그들의 서비스) = Resource Server

 

 

OAuth의 등록 

※ 클라이언트가 리소스 서버를 이용하기 위해서는 리소스서버의 승인을 사전에 받아야 함

 

 서비스마다 등록 방법이 다르지만 공통적으로 리소스서버에서 제공하는 항목과 입력해야하는 항목이 3개 있음

 - Client ID : 애플리케이션을 식별하는 식별자

 - Client Secret : 애플리케이션에 대한 비밀번호

 - Authorized Redirect URIs : 리소스서버가 권한을 부여하는 과정에서 authorize code를 전달하는데 그 코드를 전달 받는 곳의 주소, 다른 주소에서 요청하면 무시하게됨

※ 리소스서버에서 이 세가지를 찾아서 확인 및 입력.

 

 

OAuth Resource Owner의 승인

Client에서 심어둔 리소스 서버 url로 링크

 - 회원 가입, 로그인 등 리소스 서버의 기능을 사용할 Client 페이지에 리소스 서버 링크추가

 - 리소스 서버 링크 예시 :  http://resource.server/?client_id=1&scope=B,C&redirect_uri=https://client/callback

② 리소스 서버에서 리소스 서버 계정 로그인 요청

③ 유저는 리소스 서버의 계정 로그인

 - 유저가 보낸 url 파라미터에서 client_id,redirect_uri를 리소스 서버가 가지고있는 Client ID, Redirect URL를 비교하여 일치하지 않으면 끝냄

④ Client서버가 신청한 필요기능(B,C) 확인 및 동의 요청

⑤ 동의 확인

⑥ 허용한 유저 정보 추가

 - 유저가 보낸 url 파라미터로 리소스 서버에 허용한 유저 정보를 추가함

 

 

OAuth Resource Server의 승인

Resource 서버가 Authorization Code를 생성

Redirect URL을 통해 authorization code 전송

 - ex) location : https://client/callback?code=3

 - 응답할때 header 값으로 location을 보내면 Resource Owner가 알아차리지 못하게 리다이렉션이 됨

Client에게 authorization code 전송

URI파라미터를 통해 Authorization code저장

authorization_code, Client ID, Client Secret, Redirect URL 파라미터로 보내서 access token을 요청

 - ex) https://resource.server/token

         ?grant_type=authorization_code

         &code=3

         &redirect_uri=https://client/callback

         &client_id=1

         &client_secret=2

- Resource 서버는 URI의 파라미터를 통해 정상적인 요청인지 확인

 

 

OAuth 엑세스토큰 발급

Authorization Code를 삭제

 - Resource 서버와 Client는 Authorizaiton Code를 삭제

 - 다시 인증을 반복하지 않기 위해

Resource 서버가 AccessToken 발급

Client에게 access token응답

URL파라미터를 통해 AccessToken 저장

※ 클라이언트가 발급받은 access token으로 리소스 서버에 접근하게 되면 리소스 서버는 access token을 보고 해당되는 user에 대해 유효한 scope(기능)의 권한이 열려있는 것이라고 허용하게 됨

 

 

OAuth API 호출

 리소스 서버를 핸들링 하기 위해서 리소스 서버가 작성해둔 사용 방법에 맞게 호출해야함

 각 리소스 서버마다 API호출 방법이 문서로 나와있음

 

 

OAuth Refresh token

1. 리프레시 토큰이란

 엑세스토큰은 수명이 있고 그 수명이 끝나면 API에 접속했을때 데이터를 주지 않음

 그럴때마다 엑세스토큰을 다시 발급 받아야하는데 그것을 매번 사용자에게 시키기엔 불편함

 손쉽게 우리가 엑세스토큰을 발급 받을 수 있게 하는 방법이 리프레시 토큰

 

※ RFC : 인터넷과 관련된 기술의 표준안

 

2. 리프레스 토큰의 동작 방식

각 리소스 서버마다 Refresh token을 요청하여 Access Token을 재발급 받는 방법에 대한 API호출 방법이 문서로 나와있음

반응형
반응형

공통 관심 사항

1. 공통 관심 사항이란

많은 로직에서 공통으로 관심이 있는 부분을 공통 관심사(cross-cutting concerns)라 함. 

• 로그인을 하지 않은 사용자는 접근할 수 있는 페이지가 제한적이며 로그인이 필요한 페이지 접근이 허용되서는 안됨.

• 로그인이 필요한 모든 컨트롤러 로직에 로그인 여부를 확인하는 코드를 작성하기엔 비효율적
• 여러 로직에서 공통으로 로그인에 관심을 가지고 있는데, 이러한 공통 관심사는 스프링에서 AOP로 처리할 수 있음

• 웹에 관련된 공통 관심사는 스프링 AOP 보다는 서블릿 필터, 스프링 인터셉터에서 처리하는게 좋음.

• 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL 정보가 필요한데 서블릿 필터나, 스프링 인터셉터는 HttpServletRequest를 제공.

 

 

서블릿필터 VS 스프링 인터셉터

※ 서블릿 필터와 스프링 인터셉터는 모두 웹과 관련된 공통 관심사를 처리하는데 사용되는 기술
필터는 서블릿에서 제공하고 인터셉터는 스프링 MVC가 제공하는 기술인데, 적용되는 순서와 범위, 사용방법이 다름 

 

1. 필터와 인터셉터의 흐름 비교

• 필터를 적용하면 필터가 호출된 이후 서블릿이 호출됨
(여기서 서블릿은 스프링의 경우 디스패처 서블릿을 의미한다고 생각하면 된다.)
• 인터셉터를 적용하면 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출됨  
• 필터는 서블릿 호출전에 인터셉터는 서블릿 호출 이후 호출되기에 인터셉터는 서블릿에서 예외가 발생한다면 호출되지 않음

 

 

2. 필터와 인터셉터의 제한 비교

• 필터와 인터셉터는 각각 요청이 적절하지 않을경우 자신의 상태에서 종료할 수 있음
• 필터는 서블릿까지 가지 못하지만, 스프링 인터셉터는 서블릿까진 통과 후 제한됨 

 

 

3. 필터와 인터셉터 체인 비교

 

• 필터 및 인터셉터는 둘 다 추가로 적용할 수 있다. 
ex) 로그를 남기는 필터(혹은 인터셉터)를 적용 후 그 다음 로그인 여부를 체크하는 필터(혹은 인터셉터)를 만들어 적용할 수 있음


※ 호출시점의 차이를 빼면 별 차이가 없어보이지만, 스프링 인터셉터는 좀 더 편하고 정교하며 다양한 기능을 제공함.

 

 

서블릿 필터

1. 필터 인터페이스

public interface Filter {
    public default void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}

 필터 인터페이스를 구현한 뒤 등록하면 서블릿 컨테이너가 필터를 등록  후 싱글톤 객체고 생성 및 관리
- init(): 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출됨
- doFilter(): 고객의 요청이 올 때마다 해당 메서드가 호출됨
- destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출됨

※ init, destroy 메서드는 default 메서드 이기에 따로 구현하지 않아도 됨.

 

 

2. 필터 구현 예제 1

2.1 로그 필터 기능 소스

package hello.login.web.filter;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LogFilter implements Filter {
	public void init(FilterConfig filterConfig) throws ServletException {
		log.info("log filter init");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		log.info("log filter doFilter");
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		String requestURI = httpRequest.getRequestURI();
		
		String uuid = UUID.randomUUID().toString();
		try {
			log.info("REQUEST [{}] [{}]", uuid, requestURI);
			// 서블릿과 컨트롤러를 호출
			chain.doFilter(httpRequest, response);
		} catch(Exception e) {
			throw e;
		} finally {
			log.info("RESPONSE[{}] [{}]", uuid, requestURI);
		}		
	}

	public void destroy() {
		log.info("log filter destroy");
	}
}

 public class LogFilter implements Filter

 - Filter 인터페이스를 구현하며 init, doFilter, destroy 메서드를 재정의


 doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면 doFilter가 호출됨
- ServletRequest request는 HTTP 요청이 아닌 경우도 고려해서 만든 인터페이스.

- HTTP를 사용하면 HttpServletRequest로 다운캐스팅한 뒤 사용하면 됨


 UUID.randomUUID().toString()
- HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 만든다. UUID로 만드는 값이 중복될 일은 거의 없다. 

 

chain.doFilter(request, response);
- 가장 중요.

 - 다음 필터가 있으면 다음 필터를 호출하고 필터가 없으면 서블릿을 호출

- 만약 이 로직을 호출하지 않으면 다음단계로 진행되지 않음.

 

 

2.2 필터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer{
	@Bean
	public FilterRegistrationBean logFilter() {
		FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
		filterRegistrationBean.setFilter(new LogFilter());
		filterRegistrationBean.setOrder(1);
		filterRegistrationBean.addUrlPatterns("/*");
		
		return filterRegistrationBean;
	}
}

 setFilter(new LogFilter)
 - 등록 할 필터를 지정
 setOrder(1)
 - 필터는 체인으로 동작하기에 순서가 필요하다. 순서가 낮을수록 먼저 동작
 addUrlPatterns("/*")
 - 필터를 적용할 URL 패턴을 지정하며, 하나 이상의 패턴을 지정 할 수도 있음.

 

 

3. 필터 구현 예제 2

3.1 로그인 인증 체크 필터 기능 소스

package hello.login.web.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.util.PatternMatchUtils;

import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LoginCheckFilter implements Filter {
	private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*" };

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest)request;
		String requestURI = httpRequest.getRequestURI();
		
		HttpServletResponse httpResponse = (HttpServletResponse)response;
		
		try {
			log.info("인증 체크 필터 시작{}", requestURI);
			
			if(isLoginCheckPath(requestURI)) {
				log.info("인증 체크 로직 실행 {}",requestURI);
				HttpSession session = httpRequest.getSession(false);
				if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
					log.info("미인증 사용자 요청 {}",requestURI);
					
					// 로그인으로 리다이렉트, 로그인 완료 후 로그인전 페이지로 보내기위해 파라미터 담기
					httpResponse.sendRedirect("/login?redirectURL="+requestURI);
					return;
				}
			}
			chain.doFilter(httpRequest, httpResponse);
		} catch (Exception e) {
			throw e; // 예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함
		} finally {
			log.info("인증 체크 필터 종료 {}",requestURI);
		}
		
	}
	
	//화이트 리스트의 경우 인증 체크 X
	private boolean isLoginCheckPath(String requestURI) {
		// 화이트리스트와 requesURI가 패턴이 일치 하는지
		return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
	}
}

 private static final String[] whitelist = {"/", "/members/add", ...};
 - 정적 리소스(css)와 로그인, 로그아웃의 경우 로그인을 하지 않아도 접근이 가능해야 함.

 - 화이트리스트를 제외한 나머지 경로에는 인증 체크 로직을 적용 해 줌

 

 isLoginCheckPath(String requestURI)
 - 매개변수로 전달받은 requestURI가 화이트리스트와 일치하는지 검사

 - PatternMatchUtils라는 정적 헬퍼 클래스를 이용하여 쉽게 경로 검사가 가능

 

 httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
 - 로그인을 안했는데 로그인이 필요한 페이지에 접근시 로그인 페이지로 이동
 - redirectURL의 queryString : 내가 등록한 상품 목록 페이지에 접근하려는 상황에서 로그인이 안되어 있어 로그인 페이지로 이동했다고 가정할 때 로그인을 하면 다시 상품목록 페이지로 이동시켜주면 사용자 입장에선 편리함.

- 컨트롤러에서 redirectURL 관련 처리를 해줘야함

 

 return;
 - 필터를 더 진행하지 않음

 - redirect를 사용했기에 redirect가 응답으로 적용되고 요청이 끝남

 

 

3.2 필터 등록

@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
    filterRegistrationBean.setFilter(new LoginCheckFilter());
    filterRegistrationBean.setOrder(2);
    filterRegistrationBean.addUrlPatterns("/*");

    return filterRegistrationBean;
}

 addUrlPatterns("/*")

 - 허용 URL은 /*으로 전부 허용을 해 줌

 - 필터 내부에 화이트리스트가 있기 때문에 검사가 불필요한 경로는 검사를 하지 않음.

 

 

3.3 로그인 컨트롤러 (redirectURL처리)

@PostMapping("login")
public String loginV4(@Valid @ModelAttribute LoginForm form,
			BindingResult bindingResult,
			@RequestParam(defaultValue = "/") String redirectURL,
			HttpServletResponse response,
			HttpServletRequest request) {
    if (bindingResult.hasErrors()) {
        return "login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
    if (loginMember == null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
        return "login/loginForm";
    }

    //세션 매니저를 통해 세션 생성및 회원정보 보관
    //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
    HttpSession session = request.getSession();
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    if (redirectURL != null) {
        return "redirect:" + redirectURL;
    }

    return "redirect:/";
}

 로그인이 성공했을 경우 redirectURL이라는 @RequestParam이 있으면 로그인 하기 전 그 페이지로 되돌아가기위해 사용

 

 

스프링 인터셉터

1. 인터셉터 인터페이스

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

 doFilter 하나로 로직을 수행하는 서블릿 필터와는 다르게 인터셉터는 다음과 같이 3가지 단계로 세분화 되어있음.
 - 컨트롤러 호출 전(preHandler)
 - 컨트롤러 호출 후(postHandle)
 - 요청 완료 이후(afterCompletion)

 

 

2. 인터셉터 흐름

2.1 인터셉터 메서드의 흐름

 (1) preHandler: 컨트롤러 호출 전에 호출되며 반환 타입은 Boolean, 반환 값이 false이면 그 뒤는 진행하지 않음  
 (4) postHandler: 컨트롤러 호출 후 호출되며 정확히는 핸들러 어댑터 호출 후 호출. 
 (6) afterCompletion: 뷰가 렌더링 된 후에 호출

 

 

2.2 인터셉터 예외 발생 흐름

 preHandle : 컨트롤러 호출 전에 호출

 postHandler : 컨트롤러에서 예외가 발생하면 postHandler은 호출되지 않음

 afterCompletion : 항상 호출(try-catch의 finally처럼) 이전에 발생한 예외가 있을 경우 이를 파라미터로 받아서 어떤 예외가 발생했는지 확인할 수 있음

 

※ 예외가 발생하면 postHandler()같은 경우 호출되지 않기 때문에 예외 처리가 필요하다면 afterCompletion()을 사용해야 함

afterCompletion은 Exception ex를 매개변수로 받고 있으며 Nullable하기에 notNull인 경우 해당 처리를 수행하면 됨 

 

 

3. 인터셉터 구현 예제

3.1 로그 인터셉터 기능 소스

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        request.setAttribute(LOG_ID, uuid);

        //@RequestMapping: HandlerMethod가 넘어온다.
        //정적 리소스: ResourcehttpRequesthandler가 넘어온다.
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("pohstHandler [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = (String) request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);

        if (ex != null) {
            log.error("afterCompletion error:", ex);
        }
    }
}

 public class LoginInterceptor implements HandlerInterceptor

 - 인터셉터 구현은 HandlerInterceptor를 구현

 

 reqeust.setAttribute(LOG_ID, uuid)
 - 스프링 인터셉터는 호출 시점이 분리되어 있기에 각각의 메서드가 호출되는 시점에 변수들의 값 유지가 되지 않음

 - preHandler에서 지정한 값을 postHandler이나 afterCompletion에서 사용하려면 멤버변수를 사용하면 안되고 request 인스턴스에 담아두어서 사용해야함  

※ 인터셉터는 싱글톤 처럼 사용되기 때문에 멤버변수에 값을 저장하면 안됨
 - 위 코드에서 request에 담은 LOG_ID(uuid)는 afterCompletion에서 getAttribute로 찾아 사용


 HandlerMethod hm = (HandlerMethod) handler;
 - 스프링에서는 일반적으로 @Controller, @RequestMapping을 활용해 핸들러 매핑을 사용하는데, 이 경우 스프링 인터셉터의 Object handler 매개변수에는 핸들러 정보로 HandlerMethod가 넘어옴

 

 

3.2 인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }

    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }
}

WebMvcConfigurer 인터페이스를 구현하여 addInterceptor 메서드를 재정의해서 인터셉터 등록이 가능

 addInterceptor: 인터셉터를 등록
 order(1): 인터셉터의 호출 순서를 지정하며 낮을 수록 먼저 호출
 addPathPatterns("/**"): 인터셉터를 적용할 URL 패턴을 지정
 excludePathPatterns("/css/**", "/*.ico", "/error") :인터셉터에서 제외할 패턴을 지정

 

 

※ PathPattern 공식 문서

? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
{spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처


/pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/toast.html

/resources/*.png — matches all .png files in the resources directory

/resources/** — matches all files underneath the /resources/ path, including /resources/image.png and /resources/css/spring.css

/resources/{*path} — matches all files underneath the /resources/ path and captures their relative path in a variable named "path"; /resources/image.png will match with "path" → "/image.png", and /resources/css/spring.css will match with "path" → "/css/spring.css"

/resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the value "spring" to the filename variable

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

 

 

 

ArgumentResolver 활용

1. session 정보를 담는 에노테이션 구현 예시

1.1 @Login 에노테이션 생성

package hello.login.web.argumentresolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 파라미터 타입
@Target(ElementType.PARAMETER)
// 동작할때까지 남아있도록
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {

}

 @Target(ElementType.PARAMETER) : 파라미터에만 붙힐 수 있는 애노테이션
@Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있도록 해줌

 

1.2 ArgumentResolver 구현

package hello.login.web.argumentresolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import hello.login.domain.member.Member;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver{
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		log.info("supportsPararmeter 실행");
		
		// Login에노테이션이 파라미터에 있는지
		boolean hasLoginAnnotation =  parameter.hasParameterAnnotation(Login.class);
		// 파라미터의 타입이 Member 클래스의 타입인지
		boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
		
		return hasLoginAnnotation && hasMemberType;
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
				NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		log.info("resolverArgument 실행");
		HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
		HttpSession session = request.getSession(false);
		if(session == null) {
			return null;
		}
		
		return session.getAttribute(SessionConst.LOGIN_MEMBER);
	}
}

 supportsParameter()
 - 컨트롤러 호출시 각 매개변수들은 ArgumentResolver에 의해 매핑이 됨

 - 많은 ArgumentResolver가 각각 대응할 수 있는 객체는 제한되어있음

 - 이를 책임사슬 패턴을 이용해 처리

 - 각각의 ArgumentResolver는 이 메서드(supportsParameter())를 이용해 매핑가능여부를 Boolean 타입으로 반환함. 
 - 여기선 @Login 애노테이션이 붙어있고 Member객체인 경우 지원이 가능하다고 로직을 구현


 resolverArgument()
 - 실제로 컨트롤러에 필요한 파라미터 정보를 생성해주는 메서드

 - 여기서는 세션에서 로그인 회원 정보인 member 객체를 찾아 반환해줌

 

1.3 ArgumentResolver 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
		resolvers.add(new LoginMemberArgumentResolver());
	}

	// ...
}

 WebMvcConfigurer 인터페이스를 구현하여 addArgumentResolvers메서드를 재정의해서 ArgumentResolver 등록이 가능

 

1.4 controller에서 ArgumentResolver 사용 

기존 소스

public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember,
        HttpServletRequest request, Model model) {

    if (loginMember == null) {
        return "home";
    }

    model.addAttribute("member", loginMember);
    return "loginHome";
}

매번 속성들(name, required)를 작성해주는건 번거롭고, 해당 애노테이션을 통해 해당 객체에 대한 명시성이 부족

 

ArgumentResolver 적용 소스

@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
	//세션에 회원 데이터가 없으면 home
	if (loginMember == null) {
		return "home";
	}

	//세션이 유지되면 로그인으로 이동
	model.addAttribute("member", loginMember);
	return "loginHome";
}

※ 애노테이션을 활용하면 더 명시적이고 간결하게 회원정보를 찾아서 매핑해줄 수 있음
※ 회원 객체(Member)의 구조가 바뀐다면 Resolver쪽만 수정해주면 됨

※ 직접 객체 매핑 로직을 구현하면 코드가 너무 길고 차후 후임자나 다른 개발자가 보기도 쉽지 않음
※ @SessionAttribute 애노테이션을 활용하면 좀 나아지지만 목적자체가 로그인 정보를 찾는다는 특정 목적의 애노테이션이 아니기 때문에 이 역시 다른 개발자가 볼 때 단번에 이해하기는 힘들고, name이 명확하지 않으면 세션의 이 정보를 어째서 조회하는지에 대해 한번에 이해하기 어려움
※ ArgumentResolver를 이용해 애노테이션으로 요청 매핑 핸들러 어댑터를 구현해주면 하나의 특정 애노테이션으로 가독성도 좋고 편리하게 회원 정보를 조회할 수 있음

반응형
반응형

로그인 previous

※ 웹/앱 서비스에서 로그인 기능은 필수 기능

로그인 기능에는 고려할 부분이 많음

 - 아이디와 비밀번호가 맞는지 인증 기능

 - 서버에서 로그인을 처리하는 로직의 위치 파악

 - 로그인 후 로그인 페이지 리다이렉트 이전 페이지로 다시 복귀

 - 한 번 로그인 한 뒤에는 로그인 상태가 유지

 

※로그인 상태를 유지하는 방법
로그인 상태는 쿠키 혹은 세션으로 관리를 하며 여기에 해당 키의 유효시간 관리를 통해 일정시간만 유지되도록 할 수 있음

 

※ 스프링에서는 스프링 시큐리티라는 프레임워크로 로그인, 계층화, 리멤버미까지 다양한 기능을 제공하지만, 결국 이러한 스프링 시큐리티도 쿠키, 세션을 통해 관리하는 것이고 여러 리졸버를 이용함.
쿠키와 세션을 통해 로그인을 처리하는 과정과 이를 처리하기 위해 필터와 인터셉터를 학습. 

 

쿠키를 사용한 로그인 처리

1. 쿠키 동작 방식

• 서버에서 로그인 성공 시 사용자 정보를 쿠키에 담아 브라우저로 전달하면 브라우저는 해당 쿠키를 저장해둠  
• 해당 사이트에 접속할 때마다 지속해서 해당하는 쿠키를 사용

※ request의 헤더에 cookie가 담겨있어 모든 request에 쿠키 정보가 자동으로 포함됨

 

2. 쿠키의 종류

※ 사용자는 상황에따라 쿠키의 생명주기를 설정해 사용할 수 있음
 - 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
 - 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

 

3. 서버에서 쿠키 생성하기 - version 1

※ java.servlet.http에는 Cookie라는 클래스를 제공해주는데 이 클래스를 이용해 클라이언트에 응답 할 쿠키정보를 쉽게 핸들링할 수 있음

3.1 로그인 수행 controller 쿠키 생성 하기 예제

@PostMapping("login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
    if (bindingResult.hasErrors()) {
        return "login/loginForm";
    }

    Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
    if (loginMember == null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
        return "login/loginForm";
    }

    //쿠키에 시간 정보를 주지 않으면 세션 쿠키가 된다. (브라우저 종료시 모두 종료)
    Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(idCookie);

    return "redirect:/";
}

• new Cookie("memberId", String.valueOf(loginMember.getId()));
 - Cookie 라는 클래스 생성자로 key / value를 인자로 넘겨주어 생성
• response.addCookie(idCookie); 
 - 생성된 쿠키(idCookie)를 서버 응답 객체(HttpServletResponse)에 addCookie를 이용해 쿠키에 담아줌

 - 웹 브라우저에서는 Set-Cookie 프로퍼티에 쿠키정보가 담겨져 반환됨

 

 

3.2 루트 페이지 controller 쿠키 조회 하기 예제

@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
    if (memberId == null) {
        return "home";
    }

    Member loginMember = memberRepository.findById(memberId);
    if (loginMember == null) {
        return "home";
    }

    model.addAttribute("member", loginMember);
    return "loginHome";
}

• @CookieValue(name = "memberId", required = false) Long memberId
 - 쿠키를 편하게 조회할 수 있도록 도와주는 애노테이션

 - 전송된 쿠키정보중 key가 memberId인 쿠키값을 찾아 memberId 변수에 할당해줌
 - required가 false이기에 쿠키정보가 없는 비회원도 해당페이지에 접근 가능

 

 

4. 서버에서 쿠키 삭제하기(로그 아웃)

@PostMapping("/logout")
public String logout(HttpServletResponse response) {
    expiredCookie(response, "memberId");
    return "redirect:/";
}

private void expiredCookie(HttpServletResponse response, String cookieName) {
    Cookie cookie = new Cookie(cookieName, null);
    cookie.setMaxAge(0);
    response.addCookie(cookie);
}

• 로그아웃 기능은 쿠키를 삭제하는게 아니라 종료 날짜를 0으로 줘서 바로 만료시킴으로써 삭제할 수 있음

 - 응답 쿠키의 정보를 보면 Max-Age=0으로 되어있어 해당 쿠키는 즉시 종료된다.

 

 

5. version 1의 보안 문제

• 쿠키 값을 임의대로 변경할 수 있음 

 - 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 됨
 - 실제 웹브라우저 개발자모드 → Application → Cookie: memberId=1을 memberId=2로 변경(다른 사용자의 이름이 보임)
• 쿠키에 보관된 정보(memberId) 를 타인이 훔쳐갈 수 있음
• 한 번 도용된 쿠키정보는 계속 악용될 수 있음

 

6. 대안

• 쿠키에 중요한 값을 바로 노출하지 않고 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)으로 대체하고, 서버에서
토큰과 사용자 id를 매핑해서 보관

ex) 브라우저에서 cookiename : randomkey, 서버에서 randomkey : 사용자 id 

• 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예측 불가능하게 생성

• 해커가 토큰을  시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분)유지

• 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 됨.

 

 

세션을 사용한 로그인 처리

1. 세션 동작 방식

• 중요 정보는 서버의 세션 저장소에 key/value로 저장한 뒤 브라우저에서는 key값만 쿠키로 저장 해둠.

쿠키에 저장하고 있는 sessionId를 전달하면 서버의 세션저장소에서는 해당sessionId를 key로 가지고 있는 value값을 조회해서 로그인 여부와 중요 정보를 확인함

• 개선된 점

 - 회원과 관련된 정보는 클라이언트에서 가지고 있지 않음
 - 추정 불가능한 세션 아이디만 쿠키를 통해 주고받기에 보안에서 많이 안전해짐

 추가 개선할점

 - 세션아이디가 저장된 쿠키의 만료시간을 짧게 유지한다면, 해커가 해당 키를 도용한다 하더라도 금새 갱신되며 사용하지 못하게 되어 보안적으로 좀 더 안전해질 수 있음

 

2. 세션 관리 기능

※ 직접 세션을 만들기 위해서는 다음과 같이 크게 3가지 기능을 제공해야 함
 세션 생성
 - 세션 키는 중복이 안되며 추정 불가능한 랜덤 값이어야 함
 - 세션 키에 매칭될 값(사용자 정보)가 있어야 함
 - 이렇게 생성된 세션 키를 응답 쿠키에 저장해 클라이언트에 전달
 세션 조회
 - 클라이언트가 요청한 세션아이디 쿠키 값으로 세션 저장소에 저장된 값을 조회할 수 있어야 함
 세션 만료
 - 클라이언트가 요청한 세션아이디 쿠키 값으로 세션 저장소에 보관한 세션 엔트리를 제거해야 함

 

 

3. 직접 세션 구현

3.1 세션 관리 클래스 소스

package hello.login.web.session;

import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;

@Component
public class SessionManager {
	public static final String SESSION_COOKIE_NAME = "mySessionId";
	// 동시에 여러 스레드 접근 시 ConcurrentHashMap을 사용
	private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
	
	/* 세션생성
	 * sessionId생성 (임의의 추정 불가능한 랜덤값)
	 * 세션 저장소에 sessionId와 보관할 값 저장
	 * sessionId로 응답 쿠키를 생성해서 클래이언트에 전달
	 */
	public void createSession(Object value , HttpServletResponse response) {
		// sessionId생성하고, 값을 세션에 저장
		String sessionId = UUID.randomUUID().toString();
		
		sessionStore.put(sessionId, value);
		
		Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
		response.addCookie(mySessionCookie);
	}

	// 세션 조회
	public Object getSession(HttpServletRequest request) {
		Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
		if(sessionCookie == null) {
			return null;
		}
		
		return sessionStore.get(sessionCookie.getValue());
	}
	
	// 세션 만료
	public void expire(HttpServletRequest request) {
		Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
		if(sessionCookie != null) {
			sessionStore.remove(sessionCookie.getValue());
		}
		
	}
	
	// request의 쿠키를 가져와서 session과 관련된 "mySessionId"이라는 이름의 쿠키를 조회(session의 key가 들어있음)
	public Cookie findCookie(HttpServletRequest request, String cookieName) {
		/*
		Cookie[] cookies = request.getCookies();
		
		if(cookies == null) {
			return null;
		}
		
		for (Cookie cookie : cookies) {
			if(cookie.getName().equals(SESSION_COOKIE_NAME)) {
				return sessionStore.get(cookie.getValue());
			}
		}
		
		return null;
		*/
		Cookie[] cookies = request.getCookies();
		if(cookies == null) {
			return null;
		}
		// cookies 배열을 스트림으로 바꿔주고
		// 필터적용(loop로 cookies를 cookie 변수에 담아서 cookie.getName().equals(cookieName)로직을 하나씩 돌려줌)
		// findFirst() : 첫번째로 나온 cookies의 cookie객체
		// findAny() : 순서와 상관없이 빨리 나온 cookies의 cookie 객체(병렬처리에서 순서와 상관없이 찾을때)
		// 없으면 null
		return Arrays.stream(cookies)
				.filter(cookie -> cookie.getName().equals(cookieName))
				.findAny()
				.orElse(null);
	}
}

 @Component : 스프링 빈으로 자동 등록
 ConcurrentHashMap : HashMap은 동시 요청에 안전하지 않음 동시 요청에 안전한 ConcurrentHashMap를 사용

 

 

4. 직접 만든 세션 적용

4.1 로그인 컨트롤러 (세션 생성)

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
	private final LoginService loginService;
	private final SessionManager sessionManager;
    
	@PostMapping("/login")
	public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,HttpServletResponse response) {
		if(bindingResult.hasErrors()) {
			return "login/loginForm";
		}
		
		Member loginMember= loginService.login(form.getLoginId(), form.getPassword());
		
		if(loginMember ==null) {
			bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
			return "login/loginForm";
		}
		
		// 로그인 성공처리
		// 세션 관리자를 통해 세션을 생성하고 회원 데이터 보관
		sessionManager.createSession(loginMember, response);
		
		return "redirect:/";
	}
}

 로그인 성공시 직접 만든 sessionManager객체의 createSession 메서드 호출

 - sessionId(임의의 렌덤값) 생성

 - 세션 저장소에 sessionId와 보관할 값 저장

 - sessionId로 응답 쿠키를 생성해서 클라이언트에 전달

 

 

4.2 로그아웃 컨트롤러 (세션 만료)

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
	private final LoginService loginService;
	private final SessionManager sessionManager;	
    
	@PostMapping("/logout")
	public String logoutV2(HttpServletResponse response,HttpServletRequest request) {
		sessionManager.expire(request);
		return "redirect:/";
	}
}

 로그아웃 시 직접 만든 sessionManager객체의 expire메서드 호출

 - 요청 헤더의 쿠키를 찾아 sessionId로 세션 저장소에서 삭제

 

 

4.3 홈 컨트롤러 (세션 조회)

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
	private final MemberRepository memberRepository;
	private final SessionManager sessionManager;

	@GetMapping("/")
	public String homeLoginV2(HttpServletRequest request, Model model){
		// 세션 관리자에 저장된 회원 정보 조회
		Member member = (Member) sessionManager.getSession(request);
		
		// 로그인 성공
		if(member == null) {
			return "home";
		}
		
		model.addAttribute("member", member);
		
		return "loginHome";
	}
}

 

 

※ 서블릿에서는 세션매니저 역할(HttpSession)객체를 제공하고 있음

 - 우리가 만들었던 SessionManager의 역할을 하는 객체를 서블릿에서는 HttpServlet 클래스를 통해 제공하고 있음

 - HttpSession을 이용하면 우리는 세션 생성, 조회, 삭제를 편하게 사용할 수 있고 추적 불가능한 키를 가진 쿠키를 생성할 수 있음

 - 이때 쿠키 이름은 JSESSIONID이며 HttpOnly이기에 클라이언트에서 조작할 수 없음

 

 

5. 서블릿 HttpSession을 이용한 로그인 처리

5.1 세션 조회용 상수 클래스 생성

public interface SessionConst {
	String LOGIN_MEMBER = "loginMember";
}

 

5.2 로그인 컨트롤러 (세션 생성)

@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,HttpServletRequest request) {
	if(bindingResult.hasErrors()) {
		return "login/loginForm";
	}
	
	Member loginMember= loginService.login(form.getLoginId(), form.getPassword());
	
	if(loginMember ==null) {
		bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
		return "login/loginForm";
	}
	
	// 로그인 성공 처리
	// 세션이 있으면 세션 반환, 없으면 신규 세션을 생성
	// 서블릿을 통해 HttpSession을 생성하면 쿠키 이름을 JSESSIONID으로, 값은 추정 불가능한 랜덤 값으로 저장한다.
	HttpSession session = request.getSession();
	
	// 세션에 로그인 회원 정보 보관
	// 세션에 "loginMember"이름으로 loginMember 객체가 저장됨
	session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
	
	return "redirect:/";
}

 request.getSession()
 - getSession 메서드는 세션을 생성 혹은 조회하는 메서드


 public HttpSession getSession(boolean create); //default true
  - create : true일 경우
    : 세션이 있으면 기존 세션을 반환
    : 세션이 없으면 새로운 세션을 생성해 반환
  - create :false일 경우
   : 세션이 있으면 기존 세션을 반환
   : 세션이 없으면 새로운 세션을 생성하지 않고 null을 반환
※ 추가적으로 인수를 전달하지 않을 경우 기본 값으로 true

 

 

 

5.3 로그아웃 컨트롤러 (세션 만료)

@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
	// 세션이 있으면 세션 반환, 세션이 없으면 새로운 세션을 생성하지 않고 null 반환 default가 true
	HttpSession session = request.getSession(false);
	
	if(session != null) {
		// session 삭제
		session.invalidate();
	}
	
	return "redirect:/";
}

 session.invalidate();
  - 세션을 제거하는 메서드다. 

 

 

5.4 홈 컨트롤러 (세션 조회)

@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model){
	// 세션 관리자에 저장된 회원 정보 조회
	HttpSession session = request.getSession(false);
	if(session == null) {
		return "home";
	}
	
	Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
	
	// 세션에 회원 데이터가 없으면 home으로 이동
	if(loginMember == null) {
		return "home";
	}
	
	// 세션이 유지되면 로그인으로 이동
	model.addAttribute("member", loginMember);
	
	return "loginHome";
}

 HttpSession session = request.getSession(false);

 - 기존에 있는 세션 조회

 

 Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);

- "loginMember"의 key로 저장된 세션 정보 조회하여 회원 정보 저장

 

※ @SessionAttribute 어노테이션으로 위의 로직을 간편화하여 사용할 수 있음

 

 

5.5 홈 컨트롤러 (세션 조회) - @SessionAttribute 어노테이션 활용

@GetMapping("/")
public String homeLoginV3Spring(
			// @SessionAttribute어노테이션은 세션을 생성하지 않음 찾을때만 씀
			@SessionAttribute(name=SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model){

	// 세션에 회원 데이터가 없으면 home으로 이동
	if(loginMember == null) {
		return "home";
	}
	
	// 세션이 유지되면 로그인으로 이동
	model.addAttribute("member", loginMember);
		
	return "loginHome";
}

@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
 - 이전에 사용한 @CookieValue와 비슷

 - 클라이언트로부터 전달받은 내용의 세션중에서 key가 일치하는게 있는지 찾음.

 - required가 false이니 만약 못찾으면 null이 할당

※ 소스가 간결해짐

 

 

5.6 HttpSession객체에서 제공하는 정보

package hello.login.web.session;

import java.util.Date;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
public class SessionInfoController {
	@GetMapping("/session-info")
	public String sessionInfo(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if(session ==null) {
			return "세션이 없습니다.";
		}
		
		// 세션에 있는 attribute를 꺼내서 확인
		session.getAttributeNames().asIterator()
		.forEachRemaining(name -> log.info("session name={}, value= {}", name, session.getAttribute(name)));
		
		log.info("sessionId={}", session.getId());
		log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
		log.info("getCreationTime={}",new Date(session.getCreationTime()));
		log.info("getLastAccessedTime={}", new Date(session.getLastAccessedTime()));
		log.info("isNew={}", session.isNew());
		
		return "세션 출력";
	}
}

 sessionId : 세션 아이디(JSESSIONID)의 값(ex:754BE5D4DD969894D958AC278370D06E)
 maxInactiveInterval : 세션의 유효 시간(ex: 1800초, (30분))
 creationTime: 세션 생성일시
 lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접속한 시간.
(클라이언트에서 서버로 sessionId(JSESSIONID)를 요청한 경우 갱신됨.)
 isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청해서 조회된 세션인지 여부

 

 

5.7 세션 타임아웃 설정

 대부분의 사용자는 직접 명시적으로 로그아웃 버튼을 누르지 않음

HTTP는 비연결성(ConnectionLess)이기에 서버에선 클라이언트가 웹 브라우저를 종료했는지를 알 수 없음

※ 세션을 언제 삭제해야할지 판단하기 어려움  

 

• 세션의 타임아웃을 설정 되어야하는 이유

 - 세션을 무한정 유지되도록 한다면, 여러가지 문제가 발생할 수 있음

 - JSESSIONID를 탈취당한 경우 시간이 흘러도 해당 쿠키로 악용될 수 있음

 - 세션은 기본적으로 메모리에 생성되는데 메모리의 크기가 무한하지 않아 사용하지 않는 세션이 관리되지 않으면 성능 저하 발생 및 OutOfMemoryException이 발생 할 수있음

 

 세션의 종료 시점
 - 타임아웃이 너무 빠르면 로그인 유지가 무관하게 계속 로그인을 해야함.

 - 기본적으로는 세션 생성 시점으로부터 30분 정도

 - 사용자가 가장 최근 요청한 시간을 기준으로 30분 정도를 유지
※ HttpSession은 기본적으로 이 방식을 사용

 

세션의 타임아웃 설정 방법

※ 스프링 부트에서는 application.properties에 글로벌 설정을 해 줄 수 있음

session.setMaxInactiveInterval(1800);//1800초

- 1800초(30분)으로 설정을 해두면 LastAccessTime 이후 timeout 시간이 지나면 WAS 내부에서 해당 세션을 삭제

 

※ TrackingModes

 최초 로그인시 브라우저 URL 입력창이 다음과 같은 형식이됨.
http://localhost:8080/;jsessionid=F5511518B921DF6209l.......
 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해 세션을 유지하는 방법임.

 이를 없애기 위해서는 스프링 설정 파일(application.properties)에 다음과 같은 설정을 추가해주면 됨.

server.servlet.session.tracking-modes=cookie
반응형
반응형

Bean validation 소개

1. Bean Validation이란?

 Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준
 검증 애노테이션과 여러 인터페이스의 모음
 Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator임

 이름이 하이버네이트가 붙어서 그렇지 ORM과는 관련이 없음


2. 하이버네이트 Validator 관련 링크

 공식 사이트: http://hibernate.org/validator/
 공식 메뉴얼: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
 검증 애노테이션 모음: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec

 

 

Bean validation 사용하기

1. 의존성 추가 및 에노테이션 확인

 Bean Validation 기능은 라이브러리를 추가해서 사용해야 함
build.gradle에 추가

implementation 'org.springframework.boot:spring-boot-starter-validation'

※ spring-boot-starter-validation 의존관계를 추가하면 라이브러리가 추가 됨

 Jakarta Bean Validation추가 확인

- jakarta.validation-api : Bean Validation 인터페이스

- hibernate-validator : 구현체

 

 

 

 

 

 

 

 

 

 

 

2. Item -Bean Validation 에노테이션 적용 예제

package hello.itemservice.domain.item;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;

import lombok.Data;

@Data
public class Item {
	private Long id;

	// 빈값 + 공백만 있는경우를 허용하지 않음
	@NotBlank
	private String itemName;

	// null을 허용하지 않음
	@NotNull
	@Range(min = 1000, max = 1000000)
	private Integer price;

	@NotNull
	@Max(9999) // 수정 요구 사항 추가
	private Integer quantity;

	public Item() {
	}

	public Item(String itemName, Integer price, Integer quantity) {
		this.itemName = itemName;
		this.price = price;
		this.quantity = quantity;
	}
}

 @NotBlank : 빈 값 + 공백만 있는 경우를 허용하지 않음
 @NotNull: null을 허용하지 않음
 @Max(최대값): 최대값 초과를 허용하지 않음
 @Range(min, max): 범위 안의 값이여야 함 

 


※ javax.validation으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스
※ org.hibernate.validator로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증
기능

※ 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 됨

 

 

3. 테스트코드로 동작 확인

@Test
void beanValidation() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();

    Item item = new Item();
    item.setItemName(" ");
    item.setPrice(0);
    item.setQuantity(10000);


    Set<ConstraintViolation<Item>> validate = validator.validate(item);
    for (ConstraintViolation<Item> violation : validate) {
        System.out.println("violation = " + violation);
        System.out.println("violation.getMessage() = " + violation.getMessage());
    }

}

※ 임의로 validator를 꺼내서 테스트 진행 (아이템의 모든 필드의 유효성을 어긴 테스트 코드)

violation={interpolatedMessage='공백일 수 없습니다', propertyPath=itemName,
rootBeanClass=class hello.itemservice.domain.item.Item,
messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation.message=공백일 수 없습니다
        
violation={interpolatedMessage='9999 이하여야 합니다', propertyPath=quantity, 
rootBeanClass=class hello.itemservice.domain.item.Item, 
messageTemplate='{javax.validation.constraints.Max.message}'}
violation.message=9999 이하여야 합니다
        
violation={interpolatedMessage='1000에서 1000000 사이여야 합니다', propertyPath=price,
rootBeanClass=class hello.itemservice.domain.item.Item,
messageTemplate='{org.hibernate.validator.constraints.Range.message}'}
violation.message=1000에서 1000000 사이여야 합니다

※ 스프링 부트는 자동으로 글로벌 Validator로 등록함
 - spring-boot-starter-validation 라이브러리를 넣으면 스프링 부트가 자동으로 Bean Validator를 인지하고 스프링에 통합함

 - LocalValidatorFactoryBean이 글로벌 Validator로 등록되며 @Valid , @Validated 만 적용하면 됨.

 - 위에서 사용해봤던 @NotNull과 같은 애노테이션 검증을 수행.

 - 검증 오류 발생시 FieldError, ObjectError를 생성해 BindingResult에 담아줌. 

 - 임의로 글로벌 Validator를 등록해주면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않기에 위의 검증 애노테이션들이 동작하지 않음.

 

 

필드 검증하기(FieldError)

1. 필드 검증 요구 사항

 이름은 공백이여선 안됨
 가격은 빈 값이면 안되고, 1000원 이상 100만원 이하여야 함
 수량은 빈 값이면 안되고, 9999개까지만 가능

 

 

2. 검증 요구사항 에노테이션 적용

@Data
public class Item {
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1_000_000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

 

3. 컨트롤러에 적용

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item,
			BindingResult bindingResult, 
			RedirectAttributes redirectAttributes,
			Model model) {
		...
}

 @Validated 검증 애노테이션을 붙혀주고 검증결과를 담기위해 BindingResult 클래스를 바로 다음 위치에 매개변수 선언

스프링에서는 자동으로 필드에 적용된 검증 애노테이션을 수행

 

4. 검증 순서

① @ModelAttribute 각각의 필드에 타입 변환 시도
 - 성공하면 다음 필드 진행
 - 실패하면 typeMismatch로 FieldError 추가 
② Validator 적용


 Bean Validation 적용 

 - 각각의 필드에 바인딩이 성공한 필드만 Bean Validation이 적용

 - BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않음
 - 타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미 있음

※ price 에 문자 "A" 입력 → "A"를 숫자 타입 변환 시도 실패  typeMismatch FieldError 추가   price 필드는 BeanValidation 적용 안됨

 

5. 검증 메세지 수정

 Bean Validation을 사용하면서 따로 messages.properties를 설정해주거나 작성해준적이 없는데도, 메세지가 출력됨. 

 해당 라이브러리에서 지정한 기본 메세지인데, 만약 이를 임의로 바꾸고 싶다면 MessageCodeResolver의 메세지 코드를 보면 됨.
 메세지 설정에서 MessageCodeResolver는 다음과 같이 각각의 애노테이션에 대한 메세지코드가 생성됨


@NotBlank
 - NotBlank.item.itemName
 - NotBlank.itemName
 - NotBlank.java.lang.String
 - NotBlank


@Range
 - Range.item.price
 - Range.price
 - Range.java.lang.Integer
 - Range


errors.properties에 메세지 등록

#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

 - {0} 은 필드명이고, {1} , {2} ...은 각 애노테이션 마다 다름

 

 BeanValidation 메시지 찾는 순서
① 생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기
② 애노테이션의 message 속성 사용 @NotBlank(message = "공백! {0}")
③ 라이브러리가 제공하는 기본 값 사용 → "공백일 수 없습니다."

 

※ 애노테이션의 message 사용 예

@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;

 

 

객체 검증 하기 ObjectError

1. 객체 검증

 하나의 필드에 붙힐 수 없는 이런 로직상의 검증

ex) 가격과 수량의 합은 10000원 이상이어야 한다.

 

2. @ScriptAssert

2.1 사용 예제

@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
	//...
}

 정상 수행이 되고 다음과 같은 순서로 메세지 코드도 찾음
 - ScriptAssert.item
 - ScriptAssert

 

※ 이 방식은 다음과 같은 이유로 실무에서 잘 사용되지 않음
 애노테이션의 기능자체가 강하지 않아 제약이 많고 복잡
 실무에선 검증 기능이 해당 객체의 범위를 벗어나는 경우도 있는데 이 경우 대응이 어려움
 제약조건이 많아질수록 코드가 길어지는데 속성에 로직을 넣기엔 가독성이 너무 떨어지게 됨.

 

3. 직접 코드로 구현하기

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
	// ... //
	if (item.getPrice() != null && item.getQuantity() != null) {
		int resultPrice = item.getPrice() * item.getQuantity();
		if (resultPrice < 10000) {
			bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
		}
	}
	// ... //
}

※ 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장

 

 

Bean validation의 한계

1. 상황에 따라 달라지는 검증 조건

 지금까지 예제로 다뤄 본 코드는 상품 등록(POST)에 대한 부분이였고, 검증까지 무사히 완료함
상품 수정(Fetch or Put)의 검증이 등록일 경우와 달라질 수 있음

상품 등록시 전송될 내용과 수정시 전송될 내용도 상이할 확률이 높음
ex) 상품 등록시에는 아직 등록이 되지 않았기에 아이디(id)가 존재하지 않지만, 수정시에는 이미 등록된 상품을 수정하는 것이기에 id가 null이여서는 안됨(NotNull)

상품 등록시에는 수량을 1~9999개까지만 허용했지만, 등록후에는 그 외의 값으로 수정을 해도 제약이 없도록 할 수도 있음

 등록과 수정의 상이한 제약조건은 지금 기존에 작성된 상품 엔티티에서는 적용이 불가능
 이처럼 상황에 따라 달라지는 검증 조건을 스프링에서는 다음과 같이 두 가지 방법으로 이를 해결할 수 있음
 - Bean Validation의 groups 기능을 사용하기
 - 전송 객체 분리하기(ItemSaveForm, ItemUpdateForm)

groups는 한계가 명확하기에 전송 객체 분리가 일반적으로 옳은 선택지

 

2. Bean Validation - groups를 사용해 검증 분리

2.1 사용법

등록과 수정 각각의 group을 인터페이스로 만들어서 groups 라는 속성을 사용하면 됨

 

2.2 예제

등록 인터페이스 생성

package hello.itemservice.domain.item;

public interface SaveCheck {}

 

수정 인터페이스 생성

package hello.itemservice.domain.item;

public interface UpdateCheck {}

 

Item 엔티티에 groups 적용

@Data
public class Item {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
    private String itemName;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Range(min = 1000, max = 1_000_000)
    private Integer price;

    @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
    @Max(value = 9999, groups = SaveCheck.class)
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

groups는 다수의 그룹도 설정할 수 있으며 필요에따라 맞는 그룹을 선택해 검증할 수 있음

 

controller에서 필요한 검증 groups 선택

@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
		//...
}

@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
		//...
}

 - addItem에서는 상품 저장이기에 @Validated 애노테이션에 속성으로 SaveCheck.class 사용
 - editV2에서는 상품 갱신이기에 @Validated 애노테이션 속성으로 UpdateCheck.class 사용.
 - Item 객체에서는 각각 @Validated 애노테이션에 작성된 인터페이스가 선언된 검증만 수행 

 

※ 이 방식은 사실 잘 사용되지 않음
- 해당 애노테이션자체가 문제가 있는것은 아니고 등록,수정시 전달되는 내용이 상품 도메인 객체(Item)과 딱 일치하지 않음.
ex) 회원 가입을 한다고 할 땐 회원 정보에 더해 약관정보같은 추가 정보가 있을 수 있고 아직 등록하지 않기에 존재하지 않는 정보들도 있을 수 있음 

- 이런 엔티티를 사용자에게 노출시키는 것은 보안상으로도 문제가 많음

- 노출시켜도 되는 필드를 모아 View 객체를 만들어 이를 통해 데이터를 주고받고는 함.

 

※ @Valid 검증 애노테이션은 groups라는 속성이 없기 때문에 해당 기능을 사용할 수 없음

 

 

3. Bean Validation - 등록과 수정의 Form전송 객체 분리로 검증 분리

3.1 객체 분리의 장점

 각각에 상황에맞는 전용 폼 객체를 따로 만들어서 상황에 맞는 검증을 하고, 전송 객체이기에 사용자에게 노출해도 상관이 없는 객체가 됨.

 이렇게 구현 할 경우 도메인 객체로 한번 더 변환을 해서 등록이든 수정이든 해야한다는 추가 과정이 생기지만, 이 과정을 줄이고자 엔티티를 그대로 사용하는 것보다 장점이 더 큼 

 

3.2 예제

 등록 form 전송 객체 생성

@Data
public class ItemSaveForm {
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1_000_000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;
}

 

 수정 form 전송 객체 생성

@Data
public class ItemUpdateForm {
    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1_000_000)
    private Integer price;

    //수정일 경우 제약은 사라진다.
    private Integer quantity;
}

 

controller에 적용

@PostMapping("/add")
public String addItemV2(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
		// ... 생략 ... //

		// 성공 로직
		Item item = new Item();
		item.setItemName(form.getItemName());
		item.setPrice(form.getPrice());
		item.setQuantity(form.getQuantity());
        
		// ... 생략 ... //
}

@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
		//...
}

 - @ModelAttribute에 추가되는 value 속성
: 이전과 다르게 컨트롤러에서 @ModelAttribute에 item 이라는 value 속성을 작성해줌. 

만약 이를 작성해주지 않으면 규칙에 따라 MVC Model에는 itemSaveForm라는 이름으로 담기게 됨.

그렇게되면 기존에 뷰 템플릿에서 th:object 이름을 item으로 선언해줬는데 이를 itemSaveForm으로 수정해줘야 함.

 

- Form 객체의 도메인 객체 변환 작업 

: 폼 객체를 기반으로 Item 객체를 생성 및 수정해야 하기 때문에 변환 과정이 작성되야하는데, 폼 객체와 도메인 객체간의 커플링을 최소한으로 할 수 있도록 설계에 주의.
보통 폼 객체와 같은 DTO 에서 도메인을 의존하는것은 괜찮지만 반대의 경우는 괜찮지 않음.
의존의 방향은 변경이 많은곳에서 변경이 적은곳으로 향하는게 바람직.

 

 

Bean validation - HTTP 메세지 컨버터

※ Form 데이터 전송이 아닌 ajax, fetch, axios 등등 프론트 영역에서 API JSON을 요청하는경우에도 @Valid, @Validated는 HttpMessageConvert(@RequestBody)에서도 사용할 수 있음.

 

※ @ModelAttribute, @RequestBody
 - @ModelAttribute는 HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)을 다룰 때 사용
 - @RequestBody는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용

 

 

1. 예제

1.1 컨트롤러 소스

@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
    @PostMapping("/add")
    public Object addItem(@Validated @RequestBody ItemSaveForm form, BindingResult bindingResult) {
        log.info("API 컨트롤러 호출");
        if (bindingResult.hasErrors()) {
            log.info("검증 오류 발생 errors={}", bindingResult);
            return bindingResult.getAllErrors();
        }

        log.info("성공 로직 실행");
        return form;
    }
}

 

1.2 postman 테스트 요청 및 응답 정보

※ API의 경우 다음과 같은 3가지 경우가 발생할 수 있다. 

 성공 요청: 성공
 실패 요청: JSON을 객체로 생성하는 것 자체가 실패함
 검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함


 성공 요청: 성공 - 예시

 - 요청 정보

POST http://localhost:8080/validation/api/items/add
Content-Type: application/json

{"itemName":"hello", "price" : 1000 , "quantity": 100}

 - 응답 데이터

{
    "itemName": "hello",
    "price": 1000,
    "quantity": 10
}


실패 요청: JSON을 객체로 생성하는 것 자체가 실패함 - 예시

 - 요청 정보

POST http://localhost:8080/validation/api/items/add
Content-Type: application/json

{"itemName":"hello", "price": "A", "quantity": 10}

 - 응답 데이터

{
 "timestamp": "2021-04-20T00:00:00.000+00:00",
 "status": 400,
 "error": "Bad Request",
 "message": "",
 "path": "/validation/api/items/add"
}

HttpMessageConverter에서 요청 JSON을 객체로 생성하는 것 자체가 실패하는 경우 문제

지정한 객체(ex: Item)로 만들지 못하기 때문에 컨트롤러 호출이 되지 않기 때문에 Validator도 실행되지 않음

 


검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함 - 예시

 - 요청 정보

POST http://localhost:8080/validation/api/items/add
Content-Type: application/json

{"itemName":"hello", "price": 1000, "quantity": 10000}

 - 응답 데이터

[
    {
        "codes": [
            "Max.itemSaveForm.quantity",
            "Max.quantity",
            "Max.java.lang.Integer",
            "Max"
        ],
        "arguments": [
            {
                "codes": [
                    "itemSaveForm.quantity",
                    "quantity"
                ],
                "arguments": null,
                "defaultMessage": "quantity",
                "code": "quantity"
            },
            9999
        ],
        "defaultMessage": "9999 이하여야 합니다",
        "objectName": "itemSaveForm",
        "field": "quantity",
        "rejectedValue": 100000,
        "bindingFailure": false,
        "code": "Max"
    }
]

 

 

※ @ModelAttribute vs @RequestBody
 폼 전송방식으로 할 때 @ModelAttribute를 사용할 때는 타입이 불일치해도 발생하지 않는 문제가 @RequestBody를 사용할때는 발생하는 것일까?
 - HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용되기에 특정 필드가 타입이 맞지 않더라도 나머지 필드를 정상 처리할 수 있음.
 - 하지만, HttpMessageConverter는 @ModelAttribute과는 다르게 필드 단위가 아닌 객체 전체 단위로 적용되기 때문에 메세지 컨버팅이 성공해서 객체가 만들어진 다음에나 검증 애노테이션(@Valid, @Validated)이 적용됨

 

※ HttpMessageConverter 단계에서 실패하면 예외가 발생. 예외 발생시 원하는 모양으로 예외를 처리하는 방법은 예외 처리 부분에서 다룸

반응형
반응형

검증 요구사항

1. 요구사항

• 타입 검증
    ◦ 가격, 수량에 문자가 들어가면 검증 오류 처리
• 필드 검증
    ◦ 상품명: 필수, 공백X
    ◦ 가격: 1000원 이상, 1백만원 이하
    ◦ 수량: 최대 9999
• 특정 필드의 범위를 넘어서는 검증
    ◦ 가격 * 수량의 합은 10,000원 이상

 

2. 검증 로직의 필요성

• 컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것 

정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있음

 

• 참고: 클라이언트 검증, 서버 검증
 - 클라이언트 검증은 조작할 수 있으므로 보안에 취약함
 - 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해짐
 - 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
 - API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함

 

 

검증 과정

1. 상품 저장 성공

ⓞ 사용자가 상품 등록페이지에 접근한다(HTTP GET /add)
① 사용자가 상품정보를 입력 후 서버로 전송한다(HTTP POST /add)
② 상품이 성공적으로 등록된 후 Location 정보로 상품정보 상세경로를 Redirect로 응답함
③ 클라이언트에서는 응답받은 정보에 있는 Location정보로 Redirect하여 신규 상세 페이지로 이동함

 

 

2. 상품 저장 실패

ⓞ 사용자가 상품 등록페이지에 접근한다(HTTP GET /add)
① 사용자가 상품정보를 입력 후 서버로 전송한다(HTTP POST /add)
② 상품의 유효성 검증이 실패하며 검증 오류 결과가 포함된 정보를 담아 다시 상품 등록 페이지로 이동

 

※검증에서 실패하는 대표적인 경우
 - Null
 - TypeMissMatch
 - 비즈니스 요구사항에 맞지 않음
    ex) 상품의 가격은 1000원 이상이여야 하는데 500원으로 작성)

 

 

다양한 검증 방식

1. Map사용 (검증 직접 처리)

※ 서버에서 전달받은 데이터를 직접 검증하여 Map에 담아 RedirectAttributes에 담아 보내는 방법

1.1 상품 추가 controller 소스

@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
	//검증 오류 결과를 담음 
	Map<String, String> errors = new HashMap<>();

	//검증 로직
	if(item.getItemName() == null){
		errors.put("itemName", "상품 이름은 필수입니다.");
	}
	//... 기타 검증 로직

	//검증 실패시 다시 입력 폼으로 이동해야 한다.
	if (!errors.isEmpty()) {
		log.info("errors = {}", errors);
		model.addAttribute("errors", errors);
		return "validation/v1/addForm";
	}

	//검증 성공 로직
	Item savedItem = itemRepository.save(item);
	redirectAttributes.addAttribute("itemId", savedItem.getId());
	redirectAttributes.addAttribute("status", true);
	return "redirect:/validation/v2/items/{itemId}";
}

• 검증에 실패하면 errors라는 Map에 에러 내용을 담아서 model에 담아 타임리프로 반환
• @ModelAttribute 애노테이션이 붙은 Item 객체는 에러가 발생하여 다시 페이지 이동 시 그대로 다시 담겨져 전송되며 타임리프에서 이를 사용할 수 있음
• RedirectAttributes는 uri과 파라미터에 보존할 데이터를 Redirect 할 수 있음.

 

1.2 상품추가 thyemleaf 소스

•  글로벌 오류 메세지

<div th:if="${errors?.containsKey('globalError')}">
	<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>

※ Safe Navigation Operator
 - 여기에서 errors 가 null 이라면  errors.containsKey() 를 호출하는 순간 NullPointerException 이 발생
 - errors?.은 errors가 null 일때 NullPointerException이 발생하는 대신, null 을 반환하는 문법
 - th:if 에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않음

 

• 오류 메세지 적용

<div>
	<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
	<input type="text" id="itemName" th:field="*{itemName}"
		th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'"
		class="form-control" placeholder="이름을 입력하세요">
	<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
 		상품명 오류
 	</div>
 </div>

※ input의 필드 오류 처리

 - th:class를 사용하여 해당 필드에 오류가 있으면 기존에 있던 form-control 클래스에 field-error 클래스를 추가하여 반환하고 오류가 없으면 기존에 있던 form-control 클래스만 바인딩

 

※ input의 필드 오류 처리 개선

<input type="text" id="itemName" th:field="*{itemName}"
	th:classappend="${errors?.containsKey('itemName')} ? 'fielderror' : _" 
	class="form-control" placeholder="이름을 입력하세요">

 - classappend를 사용해서 해당 필드에 오류가 있으면 field-error 라는 클래스 정보를 더해서 폼의 색깔을 빨간색으로 강조

 - 만약 값이 없으면 _ (No-Operation)을 사용해서 아무것도 하지 않음

 

1.3 문제점

 타입이 안맞는 경우(ex: Integer 타입 변수에 String 타입 값을 바인딩 하려는 경우) 컨트롤러까지 가지도 못하고  400 (Bad Request) 에러가 발생하며 오류 페이지를 띄움
잘못된 타입의 값 전달시에도 오류페이지를 보여주지 않고 잘못된 부분을 사용자에게 알려야함.  
해결책 : BindingResult 클래스를 이용해 타입이 잘못된 내용에도 오류 페이지를 내보내지 않도록 할 수 있음

 

 

2. BindingResult를 이용하여 검증 1

2.1 상품 추가 controller 소스

@PostMapping("/add")
// BindingResult가 Map<String, String> errors역할을 해줌
// BindingResult는 model에 자동으로 담아줌
// BindingResult의 위치는 @ModelAttribute Item item뒤에 와야함
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
		
	// 검증 로직
	if (!StringUtils.hasText(item.getItemName())) {
		// 이전소스
		//Map<String, String> errors = new HashMap<>();
		//errors.put("itemName", "상품 이름은 필수 입니다.");

		// modelAttribute에 담길 Object명, 필드명, 메시지
		bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
	}
	// 다른 로직
	// ... 생략 ...
	
	// 특정 필드가 아닌 복합 룰 검증
	if(item.getPrice() != null && item.getQuantity() != null) {
		int resultPrice = item.getPrice() * item.getQuantity();
		if(resultPrice < 10000) {
			// 이전소스
			//errors.put("globalError", "가격 * 수량은 10,000원 이상이어야 합니다. 현재값 = "+resultPrice);
			bindingResult.addError(new ObjectError("item", "가격 * 수량은 10,000원 이상이어야 합니다. 현재값 = "+resultPrice));
		}
	}
	
	// 검증에 실패하면 다시 입력 폼으로
	if(bindingResult.hasErrors()) {
		log.info("errors = {}", bindingResult);
		// 이전소스
		//model.addAttribute("errors",errors);
		return "validation/v2/addForm";
	}
	
	// 성공 로직
	Item savedItem = itemRepository.save(item);
	redirectAttributes.addAttribute("itemId", savedItem.getId());
	redirectAttributes.addAttribute("status", true);
	return "redirect:/validation/v2/items/{itemId}";
}

 컨트롤러의 매핑 메서드에서 BindingResult를 매개변수로 받음으로써 타입 불일치에 대한 대응 가능
BindingResult 매개변수는 반드시 전송받을 객체(ex: @ModelAttribute Item item) 다음에 위치해야 함 

 bindingResult의 addError 메서드를 이용해 에러내용을 담을 수 있음
 - @ModelAttribute 필드(ex: name, price, quantity, ...)에러인 경우 FieldError객체를 이용해 담으면 됨.


 필드 에러 (FieldError) 생성자 요약

public FieldError(String objectName, String field, String defaultMessage) {}

 - objectName: @ModelAttribute 이름
 - field: 오류가 발생한 필드 이름
 - defaultMessage: 기본 오류 메세지

 

※ 글로벌 오류인 경우 ObjectError객체를 이용해 담으면 된다. 
ObjectError 생성자 요약

public ObjectError(String objectName, String defaultMessage) {}

 - objectName : @ModelAttribute의 이름

 - defaultMessage : 오류 기본 메시지

 

 

2.2 상품추가 thyemleaf 소스

<form action="item.html" th:action th:object="${item}" method="post">
	<div th:if="${#fields.hasGlobalErrors()}">
		<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">
			글로벌 오류 메시지
		</p> 
	</div>
	<div>
		<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
		<input type="text" id="itemName" th:field="*{itemName}" 
			th:errorclass="field-error" class="form-control"
			placeholder="이름을 입력하세요">
		<div class="field-error" th:errors="*{itemName}">상품명 오류</div>
	</div>

${#fields}: BindingResult가 제공하는 검증 오류에 접근이 가능하다.
 th:errors: 해당 필드에 오류가 있는 경우 태그를 출력 (th:if 편의 버전)
 th:errorclass: th:field에서 지정한 필드에 오류가 있으면 class정보를 추가

 

※ BindingResult를 사용할 경우 클라이언트에서 타입이 잘못된 내용이 전송되더라도 BindingResult에 그 오류내용(FieldError)을 담아서 컨트롤러를 정상 호출함

※  BindingResult의 내용은 자동으로 Model에 담겨지기 때문에 타임리프에서도 자연스럽게 사용 가능

 

2.3 문제점

 사용자가 잘못 입력해서 전송한 데이터가 남아있지 않음 (사용자 입력 값을 유지할 수 없음)

 사용자가 잘못 입력한 내용이 뭔지 잊을수도있고, 혹은 에러내용을 봐도 에러 내용이 자세하지 않으면 내가 어디가 어떻게 잘못 입력했는지 파악하기 힘들어짐  
 매번 에러 메세지를 하드코딩으로 입력해야하는것도 쉽지 않고 코드 중복이 심함
해결책 : 위에서 사용한 FieldError에 오버로딩 된 생성자가 존재함 

 

 

3. BindingResult를 이용하여 검증 2 (사용자가 입력한 값을 유지하는 방법)

3.1 FieldError에 오버로딩 된 생성자 분석

public FieldError(String objectName,               // 오류가 발생한 객체 이름
                  String field,                    // 오류 필드
                  @Nullable Object rejectedValue,  // 사용자가 입력한 값(거절된 값)
                  boolean bindingFailure,          // 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값 
                  @Nullable String[] codes,        // 메세지 코드
                  @Nullable Object[] arguments,    // 메세지에서 사용하는 인자
                  @Nullable String defaultMessage) // 기본 오류 메세지.

//사용 예
new FieldError( "item", 
	        "itemName", 
	        item.getItemName(),	// 사용자가 입력 한 값 (거절된 값)
	        false,
	        null,
	        null,
	        "상품 이름은 필수입니다.")

 

3.2 controller 소스

@PostMapping("/add")
// BindingResult의 fieldError에 입력한 값 다시 넣어주는 오버로딩된 메서드 활용
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
		
	// 검증 로직
	if (!StringUtils.hasText(item.getItemName())) {
		// feildError의 파라미터 : 오류가 발생한 객체이름, 오류 필드, 사용자가 입력한값(거절된값), 타입오류 같은 바인딩 실패인지 검증실패인지 구분값, 메시지코드, 메시지에서 사용하는 인자, 기본 오류 메시지
		bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다."));
	}

	// 다른 로직
	// ... 생략 ...
		
	// 검증에 실패하면 다시 입력 폼으로
	if(bindingResult.hasErrors()) {
		log.info("errors = {}", bindingResult);
		return "validation/v2/addForm";
	}
	
	// 성공 로직
	Item savedItem = itemRepository.save(item);
	redirectAttributes.addAttribute("itemId", savedItem.getId());
	redirectAttributes.addAttribute("status", true);
	return "redirect:/validation/v2/items/{itemId}";
}

 bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다."));

 - 사용자가 입력한 값 (거절된 값을 3번째 인자로 전달)

 

 

3. BindingResult를 이용하여 검증 3 (errors.proerties로 에러 메세지 관리)

3.1 FieldError에 오버로딩 된 생성자 분석

public FieldError(String objectName,               // 오류가 발생한 객체 이름
                  String field,                    // 오류 필드
                  @Nullable Object rejectedValue,  // 사용자가 입력한 값(거절된 값)
                  boolean bindingFailure,          // 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값 
                  @Nullable String[] codes,        // 메세지 코드
                  @Nullable Object[] arguments,    // 메세지에서 사용하는 인자
                  @Nullable String defaultMessage) // 기본 오류 메세지.

//사용 예
new FieldError( "item", 
	        "price", 
	        item.getPrice(),	
	        false,
	        new String[]{"range.item.price"},    // 메세지 코드
	        new Object[]{1000, 1000000},         // 메세지에서 사용하는 인자
	        null)

 

3.2 메세지 코드 사용

스프링 부트 메세지 설정 추가 (application.properties) 파일

spring.messages.basename=messages,errors

 

resources/errors.properties 파일 추가

required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

 

3.3 controller 소스

@PostMapping("/add")
// error.properties 기능 사용
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
	// 다른 로직
	// ... 생략 ...
		
	// 검증 로직
	if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
		// new Object[]{1000, 100000}는 properties 입력한 값의 {0} {1}의 치환 인자 값
		bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
	}
		
	// 다른 로직
	// ... 생략 ...

	// 검증에 실패하면 다시 입력 폼으로
	if(bindingResult.hasErrors()) {
		log.info("errors = {}", bindingResult);
		return "validation/v2/addForm";
	}
	
	// 성공 로직
	Item savedItem = itemRepository.save(item);
	redirectAttributes.addAttribute("itemId", savedItem.getId());
	redirectAttributes.addAttribute("status", true);
	return "redirect:/validation/v2/items/{itemId}";
}

bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[] "range.item.price"}, new Object[]{1000, 1000000}, null));

 - 사용할 메세지 코드 (거절된 값을 5번째 인자로 전달)

 - 사용할 메세지 코드에 넘길 인자 (거절된 값을 6번째 인자로 전달)

 

3.4 문제점

 소스 작성이 너무 번거롭고 에러하나 담는데 넣어야 할 속성도 너무 많음
 messages의 이름도 range.item.price을 매번 다 적는것도 번거로움
해결책 : BindingResult에서는 rejectValue(), reject() 메서드를 통해 FieldError, ObjectError을 직접 생성하지 않아도 되도록 해줌

 

 

4. BindingResult를 이용하여 검증 4 (rejectValue(), reject() 메서드)

4.1 사용 예시

//before
bindingResult.addError(new FieldError("item", "itemName",item.getItemName(), false, new String[]{"required.item.itemName"}, null, null))
bindingResult.addError(new FieldError("item", "price", item.getPrice(),false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null))

//after
bindingResult.rejectValue("itemName", "required");
bindingResult.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);



// before
bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[] {10000, resultPrice}, null));

// after
bindingResult.reject("totalPriceMin", new Object[] {10000, resultPrice}, null);

 

4.2 rejectValue 메서드의 매개 변수

void rejectValue(@Nullable String field,         // 오류 필드명
		String errorCode,                // MessageResolver를 위한 오류 코드
		@Nullable Object[] errorArgs,    // 오류 메세지에서 {0}을 치환하기 위한 값
		@Nullable String defaultMessage);// 오류 메세지를 못찾을 경우 기본 메세지

 

4.3 reject 메서드 매개 변수

void reject(String errorCode,                    // MessageResolver를 위한 오류 코드
		@Nullable Object[] errorArgs,    // 오류 메세지에서 {0}을 치환하기 위한 값
		@Nullable String defaultMessage);// 오류 메세지를 못찾을 경우 기본 메세지

※ FieldError() 를 직접 다룰 때는 오류 코드를 range.item.price 와 같이 모두 입력했으나 rejectValue()를 사용하고 부터는 오류 코드를 range 로 간단하게 입력함.

 

 field와 errorCode 매개변수를 가지고 errors.properties에서 메세지를 찾아낸다는 것인데, rejectValue()와 rejct()는 내부에서 MessageCodesResolver를 통해서 찾아냄.

 

 

MessageCodesResolver

1. MessageCodesResolver 인터페이스 분석

 스프링에서 제공하는 마커 인터페이스인 MessageCodesResolver는 다음과 같은 메서드가 정의되어 있음

public interface MessageCodesResolver {
	String[] resolveMessageCodes(String errorCode, String objectName);
	String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType);
}

※ 이 인터페이스의 기본 구현체로 DefaultMessageCodesResolver를 제공하는데 이를 이용해서 각종 메세지에 대한 대처가 쉽게 가능함.


2. MessageCodesResolver의 동작

 메세지 혹은 예외메세지는 특정 필드에 맞는 메세지가 있을수도 있지만 범용성이 높은 메세지도 있을 수 있음

 예를들어 required.item.itemName=상품 이름은 필수 입니다. 라고 디테일하게 에러 메세지를 작성할 수 있지만,

required=필수 값입니다. 라고 범용적인 메세지를 작성할수도 있음.

 범용성의 수준에따라 단계를 만들어두면 MessageCodesResolver는 범용성이 낮은순서에서 높은순서로 차례대로 찾으면서 처음 매칭되는 결과를 가져옴

 

 메세지 예시

#level 1
required.item.itemName: 상품 이름은 필수입니다.

#level 2
required: 필수 값 입니다.

 - MessageCodesResolver는 디테일한순서부터 차례대로 찾음

 - 만약 level1이 작성되어있지 않다면 level2의 required값을 찾아서 담음

 - 이렇게 작성하면 오류메세지에 대한 대응이 한결 편해짐

 

2. DefaultMessageCodesResolver의 기본 매세지 생성 규칙

※ 객체 오류와 필드 오류를 범용성 순으로 찾음.
2.1 객체 오류

객체 오류의 경우 다음 순서로 2가지 생성 
1.: code + "." + object name 
2.: code

예) 오류 코드: required, object name: item 
1.: required.item
2.: required


2.2 필드 오류
※ 필드 오류의 경우 다음 순서로4가지 메시지 코드 생성

1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code

예) 오류 코드: typeMismatch, object name "user", field "age", field type: int 
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"

※ 구체적인 것에서 덜 구체적인 순으로 찾음

 

2.3 예시

 reject("totalPriceMin") ObjectError  발생 시
다음 2가지 오류 코드를 자동으로 생성

new String[]{"totalPriceMin.item", "totalPriceMin"}을 내부에서 만들어 메세지를 찾음
 - totalPriceMin.item
 - totalPriceMin

 

 rejectValue("itemName", "required") FieldError 발생 시 
다음 4가지 오류 코드를 자동으로 생성

new String[]{"required.item.itemName", "required.itemName", "required.java.lang.String", "required"} 를 내부에서 만들어 메세지를 찾음
 - required.item.itemName
 - required.itemName
 - required.java.lang.String
 - required


 오류 메시지 출력
 - 타임리프 화면을 렌더링 할 때 th:errors가 실행

 - 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾음

 - 없으면 디폴트 메시지 출력

 

 

3. MessageCodesResolver 사용해보기(테스트)

3.1 객체 오류 조회 해보는 테스트

@Test
void messageCodesResolverObject() {
	DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
	String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
	for (String msg : messageCodes) {
		System.out.println(msg);
	}
}

 - required.item
 - required

 

 

3.2 필드 오류 조회 해보는 테스트

@Test
void messageCodesResolverField() {
	DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
	String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
	for (String msg : messageCodes) {
		System.out.println(msg);
	}
}

 - required.item.itemName
 - required.itemName
 - required.java.lang.String
 - required

 

 

ValidationUtils

※ 유효성 검증을 더 편하게 작성할 수 있는 객체

//before
if (!StringUtils.hasText(item.getItemName())) { 
	bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다."); 
}

//after
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");

 너무 복잡한 검증은 힘들고 위 코드처럼 단순한 Empty나 공백처리같은 기능만 제공
 내부적으로 rejectValue를 호출하고 위에서 우리가 소개한 여러 방식들을 사용해서 에러를 담음
(MessageCodesResolver, new FiledError(), ...)

 

 

 

스프링에서 제공하는 기본 오류 메세지

※ 직접 정의한 오류 코드는 rejectValue()를 직접 호출해서 담아주지만, 스프링이 직접 검증 오류에 추가한 경우도 있음 (주로 타입 정보 불일치)

스프링에서 직접 검증 오류에 추가를 한 것으로 BindingResult에 FieldError가 다음과 같은 메세지코드가 생성되어 추가됨.

codes[
typeMismatch.item.price, 
typeMismatch.price, 
typeMismatch.java.lang.Integer, 
typeMismatch
]

※ 스프링은 타입 오류가 발생하면 자동으로 위 오류 코드들을 사용하게 됨

 errors.properties에는 해당 내용으로 정의한 메세지가 없기 때문에 스프링에서 정의한 기본 메세지가 출력됨

※ 하지만, 기본 메세지는 너무 장황하고 길어서 개발자가아닌 사용자에게 노출해서는 안됨
 errors.properties에 다음과 같이 메세지를 추가

typeMismatch.java.lang.Integer=숫자를 입력해주세요. 
typeMismatch=타입 오류입니다.



Validator 분리

※ 검증 로직은 중복이 많고, 매번 필요할때마다 작성하는것은 비효율적이지만 중요도가 높은만큼 생략할수도 없음

※ 이런 검증 로직을 별도의 클래스로 분리해서 모듈화하면 재사용성이 높아지고 가독성또한 높아질 수 있음.

 

1. 분리 예제 1

1.1 validator 인터페이스 분석

public interface Validator {
	boolean supports(Class<?> clazz);
	void validate(Object target, Errors errors);
}

 인터페이스는 책임 사슬 패턴에서 주로보이는 메서드인 supports와 실제 검증을 수행하는 validate메서드를 정의하고있음.

Validator 인터페이스를 구현하면서 Item에 대한 검증로직을 구현

 

1.2 ItemValidator 객체 생성

@Component
public class ItemValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        // isAssignableFrom을 사용하면, class와 자식 클래스까지 검증함
        // item == clazz
        // item == subItem
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;

        ValidationUtils.rejectIfEmpty(errors, "itemName", "required");

        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1_000_000) {
            errors.rejectValue("price", "range", new Object[]{1000, 1_000_000}, null);
        }
        if (item.getQuantity() == null || item.getQuantity() >= 9999) {
            errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }
        //복합 룰 검증
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }
    }
}

 Item.class.isAssignableFrom(clazz): 해당 Validator 구현체는 Item 클래스에 대한 검증을 수행할 수 있음을 의미
 Errors errors : 매개변수타입인 Errors는 BindingResult클래스의 부모 타입이기 때문에 공변성이 성립함


※ itemValidator는 Component라 Component Scan으로 등록되었기 때문에 Dependency Injection을 받아서 컨트롤러에서 사용가능

 

1.3 controller에서 itemValidator사용 법

@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {

    private final ItemRepository itemRepository;
    private final ItemValidator itemValidator;

        ...

    @PostMapping("/add")
    public String addItemV5(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        itemValidator.validate(item, bindingResult);				

        //검증 실패시 다시 입력 폼으로 이동해야 한다.
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "validation/v2/addForm";
        }
        
        ...
        
    }
}

※ 컨트롤러에 있던 검증 로직이 itemValidator.validate()메서드 호출로 검증이 가능

 

 

2. 분리 예제 2 - 에노테이션

※ 스프링에서는 Validator 인터페이스를 구현해서 검증로직을 만들면 추가적으로 애너테이션을 사용하여 검증을 수행할수도 있음

 WebDataBinder를 이용하는 것인데 이 클래스는 스프링의 파라미터 바인딩의 역할 및 검증 기능도 내부에 포함하는 클래스

 객체에 내가 만든 검증기를 추가(add)하면 자동으로 검증기 적용이 가능

 

2.1 한 컨트롤러에만 적용 - 예제1

@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {

	private final ItemRepository itemRepository;
	private final ItemValidator itemValidator;

	// 컨트롤러가 호출 될때마다 항상 실행됨
	@InitBinder
	// item 객체에 파라미터 바인딩 해주고, 검증기를 가지고 검증을 해줌
	// Spring MVC가 내부에서 검증기를 적용
	public void init(WebDataBinder dataBinder) {
		dataBinder.addValidators(itemValidator);
	}
    
	...
    
	@PostMapping("/add")
	// @Validated : item에 대해서 자동으로 검증기가 수행이됨 
	//자동 검증
	public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

	//검증 실패시 다시 입력 폼으로 이동해야 한다.
	if (bindingResult.hasErrors()) {
		log.info("errors = {}", bindingResult);
		return "validation/v2/addForm";
	}
	...
}
    
}

 WebDataBinder에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있음

 @InitBinder → 해당 컨트롤러에만 영향을 줌

@Validated 어노테이션으로  item에 대해서 자동으로 검증기가 수행됨

 

2.2 글로벌 설정 방법

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
	public static void main(String[] args) {
		SpringApplication.run(ItemServiceApplication.class, args);
	}

	@Override
	public Validator getValidator() {
		return new ItemValidator();
	}
}

 

2.3 @Validated, @Valid
 @Validated : org.springframework.validation.annotation.Validated가 스프링 전용 검증 애너테이션
 @Valid : javax.validation.@Valid는 자바 표준 검증 애너테이션

 둘 다 역할은 동일하지만, @Valid는 build.gradle에 다음과 같은 의존성을 추가해줘야 함

implementation 'org.springframework.boot:spring-boot-starter-validation'
반응형

+ Recent posts