서버가 클라이언트의 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다고 한다. 이 세가지가 헷갈려 알아보았고, 하나씩 설명을 해보겠다.
쿠키는 Key-Value
형식의 문자열 덩어리이다.
클라이언트가 어떠한 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 클라이언트의 브라우저에 설치되는 작은 기록 정보 파일
이다. 각 사용자마다의 브라우저에 정보를 저장해 고유 정보 식별을 가능하게 한다.
쿠키의 인증 방식은
- 클라이언트가 서버에 접속을 한다
- 서버는 클라이언트의 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cookie에 담는다
- 이후 해당 클라이언트는 요청을 보낼 때마다, 매번 저장된 쿠키를 요청 헤더의 Cookie에 담아 보내 서버는 쿠키에 담긴 정보를 바탕으로 해당 요청의 클라이언트가 누군지 식별한다
보안에 취약
하다용량 제한
이 있어 많은 정보를 담을 수 없다쿠키의 보안적인 이슈를 해결하기 위해 세션은 비밀번호 등 클라이언트의 민감한 인증 정보를 브라우저가 아닌 서버 측에 저장하고 관리
한다.
서버의 메모리나 서버의 로컬 파일, 데이터베이스에 저장한다
세션 객체는 Key에 해당하는 SESSEION ID와 이에 대응하는 Value로 구성되어 있으며, Map 형태로 저장된다.
세션의 인증 방식은
1. 유저가 웹사이트에서 로그인하면 세션이 서버 메모리/ 데이터베이스 상에 세션을 식별하기 위한 Session Id를 기준으로 정보를 저장한다
2. 서버에서 브라우저에 쿠키에 Session Id를 저장한다
3. 쿠키에 정보가 담겨있기 때문에 브라우저는 해당 사이트에 대한 모든 Request에 Session ID를 쿠키에 담아 전송한다
4. 서버는 클라이언트가 보낸 Session Id와 서버 메모리로 관리하고 있는 Session Id를 비교하여 인증을 수행한다
세션 ID 자체를 탈취
하여 클라이언트인척 위장할 수 있는 한계가 존재한다토큰 기반 인증 시스템은 클라이언트가 서버에 접속을 하면 서버에서 해당 클 클라이언트에게 인증 되었다는 의미로 토큰
을 부여한다. 이 토큰은 유일
하며 토큰을 발급받은 클라이언트는 또 다시 서버에 요청을 보낼 때 요청 헤더에 토큰을 심어서 보낸다. 그러면 서버에서는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰과의 일치 여부를 체크하여 인증 과정을 처리한다
기존의 세션기반 인증과의 차이점은 세션기반 인증은 서버가 파일이나 데이터베이스에 세션정보를 가지고 있어야 하고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생하지만 토큰 인증 방식은 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있다.
토큰은 앱과 서버가 통신 및 인증할 때 가장 많이 사용된다. 그 이유는 웹에는 쿠키와 세션이 있지만 앱에서는 없기 때문이다
Token 인증 방식의 순서는
1. 사용자가 아이디와 비밀번호로 로그인을 한다
2. 서버 측에서 클라이언트에게 유일한 토큰을 발급한다
3. 클라이언트는 서버 측에서 전달받은 토큰을 쿠키나 스토리지에 저장해 두고, 서버에 요청을 할 때마다 해당 토큰을 HTTP 요청 헤더에 포함시켜 전달한다
4. 서버는 전달받은 토큰을 검증하고 요청에 응답한다
JWT란 인증에 필요한 정보들을 암호화시킨 JSON 토큰
을 의미한다. JWT 토큰을 HTTP 헤더에 실어 서버가 클라이언트를 식별하는 방식이다.
사용자가 JWT를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며, 검증이 완료되면 요청한 응답을 돌려준다
JWT의 구조는 .
을 구분자로 나누어지는 세 가지 문자열의 조합이다
Header에는 JWT에서 사용할 타입과 해시 알고리즘의 종류가 담겨있으며, Payload는 서버에서 첨부한 사용자 권한 정보와 데이터가 담겨있다.
Signature에는 Header, Payload를 Base64 URL-safe Encode를 한 이후 Header에 명시된 해시함수를 적용하고 개인키로 서명한 전자서명이 담겨있다
JWT를 이용한 인증과정은
1. 사용자가 ID, PW를 입력하여 서버에 로그인 인증을 요청한다
2. 서버에서 클라이언트로부터 인증 요청을 받으면, Header, PayLoad, Signature를 정의한다
3. 클라이언트는 서버로부터 받은 JWT를 로컬 스토리지에 저장하고 API를 서버에 요청할 때 Authorization header에 Access Token을 담아서 보낸다
4. 서버가 할 일은 클라이언트가 Header에 담아서 보낸 JWT가 내 서버에서 발행한 토큰인지 일치 여부를 확인하고 인증이 통과되었으면 페이로드에 있는 유저의 정보들을 select해서 클라이언트에 돌려준다
5. 클라이언트가 서버에 요청을 했는데, 만일 액세스 토큰의 시간이 만료되면 클라이언트는 리프레시 토큰을 이용해 서버로부터 새로운 액세스 토큰을 발급 받는다
DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 / 인터페이스)로 참조
하라는 원칙이다
객체들이 서로 정보를 주고 받을 때는 의존 관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 하는 약속이 있다.
여기서의 원칙이 DIP 원칙으로 추상성이 낮은 클래스보다 추상성이 높은 클래스와 통신을 해야 한다는 것이다
클라이언트가 상속 관계로 이루어진 모듈을 사용할 때, 하위 모듈을 직접 인스턴스를 가져다 쓰지 말라는 뜻이다 그 이유는 하위 모듈의 구체적인 내용에 클라이언트가 의존하게 되어 하위 모듈에 변화가 있을 때마다
클라이언트나 상위 모듈의 코드를 자주 수정
해야 되기 때문이다.
생성자 어노테이션은 Lombok
에서 제공하는 어노테이션으로 RequiredArgsConstructor
, NoArgsConstructor
, AllArgsConstructor
세 가지가 있다
제일 기본인 생성자 어노테이션으로 아무 인수가 없는 생성자를 생성해준다. DTO를 DB와 연결해서 사용할 때나 API를 호출하는 곳에서 파라미터를 DTO로 받아올 때 기본적으로 아무 인수가 없는 생성자라도 있어야해서 DTO에 제일 많이 사용한다
꼭 필요한 객체의 변수를 인수로 받는 생성자를 구현해준다. 꼭 필요한 객체의 변수란 final 또는 @NotNull 어노테이션이 붙은 변수
를 의미한다
해당 객체 내에 있는 모든 변수들을 인수로 받는 생성자를 만들어내는 어노테이션이다. 이 어노테이션보다 builder 패턴을 사용하는 것이 코드 가동성에 좋기 때문에 많이는 사용하지는 않는다
DAT와 DTO를 설명하기 위해서는 Entity를 먼저 알아야 한다.
Entity란 DB의 테이블에 존재하는 Column들을 필드로 가지는 객체를 뜻한다. Entity는 DB의 테이블과 1대 1 대응이며, 테이블에 가지지 않는 칼럼을 필드로 가져서는 안된다.
@Entity
public class Employee{
@Id
private Long id;
private String name;
private int age;
}
DTO는 말 그대로 데이터를 Transfer하기 위한 객체이다. 클라이언트가 컨트롤러에 요청을 보낼 때도 RequestDto의 형식으로 데이터가 이동하고, Controller가 Client에게 응답을 보낼 때도 ResponseDto의 형태로 데이터를 보내게 된다.
DTO는 로직을 갖고 있지 않는 순수한 데이터 객체이며, 일반적으로 getter/setter 메서드만을 가진다. DTO는 로직을 갖고 있지 않는 순수한 데이터 객체이다.
View Layer와 DB Layer의 역할을 분리하기 위해서
객체를 표현하기 위한 계층과 저장하는 계층의 역할을 분리하기 위해서 DTO를 사용한다
Entity 객체의 변경을 피하기 위해서
Entity 객체를 그대로 사용하면 프로그래머의 의도와 다르게 데이터가 변질될 수 있다
View와 통신하는 DTO 클래스는 자주 변경된다
Domain 모델링을 지키기 위해서
도메인 설계를 잘했다고 하더라도 원하는 데이터를 표시하기가 쉽지 않다. 예를 들어 Entity 클래스의 특정 컬럼들을 조합하여 특정 포멧을 출력하고 싶을 때, Entity 클래스에 표현을 위한 필드나 로직이 추가되면 객체 설계를 망가뜨릴 수 있다.
DAO는 말 그대로 실제 DB에 접근하는 객체를 뜻한다. 한 마디로 JPA를 사용할 때 만드는 Repository가 DAO의 역할을 한다. 물론 DAO와 Repository에 차이점은 있다. 간단하게 이해를 하기 위해서 정리를 하면 하나의 Repository 내부에서 다수의 DAO를 호출하는 방식으로 Repository를 구현할 수 있다
JPA 연관관계가 내가 2주 정도의 시간동안 팀프로젝트를 하면서 가장 이해를 하기 어려웠던 부분이고 이해하기도 어려웠던 개념이었다.
DB 테이블은 외래 키 하나로 양쪽 테이브 조인이 가능
하다.
따라서 DB는 단방향이니 양방향이니 나눌 필요가 없다 그러나 객체는 참조용 필드
가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계
, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계
라고 한다.
단방향과 양방향 관계를 결정하는 방법은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해보면 된다
Board.getPost()처럼 참조가 필요하면 Board -> Post 단방향 참조
post.getBoard()처럼 참조가 필요하면 Post -> Board 단방향 참조
이런식으로 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것이다.
불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있다
두 객체가 양방향 관계일 때 연관 관계의 주인을 지정해야한다.
두 단방향 관계중 제어의 권한(외래 키를 비롯한 테이블 레코드를 저장, 수정, 삭제 처리
)를 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려주는 것이다.
연관관계의 주인은 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만 주인이 아니면 조회만 가능하다
연관 관계의 주인이 아닌 객체에서 mappedBy 속성을 사용해서 주인을 지정해줘야 한다
데이터베이스
를 기준으로 다중성을 결정한다
요구사항
DB를 기준으로 다중성(게시글 N : 게시판 1)을 결정했고, 외래 키를 게시글이 관리하는 일반적인 형태이다. (다(N) 쪽이 외래 키를 갖는다!!)
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
}
단방향이기 때문에 다 쪽인 Post에만 @ManyToOne만 추가
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
일대다가 다대일의 그저 반대 입장으로 쓴 것이 아니라 다대일의 기준은 연관관계의 주인 다(N)쪽에 둔 것이고 일대다는 연관관계의 주인을 일(1)쪽에 둔 것이다.
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany
@JoinColumn(name = "POST_ID") //일대다 단방향을 @JoinColumn필수
List<Post> posts = new ArrayList<>();
//... getter, setter
}
@OneToMany에 양방향이 아니기 때문에 mappedBy를 쓰지 않고 @JoinColumn
을 이용해서 조인을 한다
주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래키를 넣을 수도 있다.
일대일 단방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
//... getter, setter
}