// 에드센스

이 카테고리는 인프런 김영한님의 JPA 강의를 보고 정리하는 공간입니다.


@Entity

  • @Entity 어노테이션이 붙은 클래스는 JPA가 관리한다. 이를 엔티티라고 한다.
  • 이 엔티티와 데이터베이스의 테이블이 매핑된다.
  • 엔티티 클래스에는 기본 생성자가 있어야한다.
  • final 클래스, enum, interface, inner 클래스는 안된다.
  • 저장할 필드에 final키워드는 안된다.
  • name 속성을 사용하여 엔티티 이름을 커스터마이징 할 수 있다. (중복되는 이름의 클래스가 없다면 기본값 권장)

 

 

 


데이터베이스 스키마 자동 생성

  • create: 기본 테이블 삭제 후 다시 생성(DROP + CREATE)
  • create-drop: create와 같으나 애플리케이션 종료 시점에 DROP
  • update: 변경사항만 반영(운영중인 DB에는 사용하면 안됨)
  • validate: 엔티티와 테이블이 정상적으로 매핑됐는지만 확인
  • none: 사용x

개발 초기에는 create나 update를,

테스트 서버에는 update나 validate를,

운영 서버에는 validate나 none을 권장한다.

 

 

 


필드와 컬럼 매핑

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
    @Id
    private Long id;
    
    @Column(name = "name")
    private String username;
    
    private Integer age;
    
    @Enumerated(EnumType.STRING)
    private RoleType roleType;
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
    
    @Lob
    private String description;
}

 

  • @Colunm: 해당 필드와 명시한 컬럼 매핑
  • @Temporal: 날짜 타입 매핑
  • @Enumerated: enum타입 매핑
  • @Lob: 대용량 매핑, CLOB, BLOB이 있다.
  • @Transient: 해당 필드는 매핑하지 않겠다.

 

여기서 Enumerated 매핑 사용시 EnumType.ORDINAL 옵션은 사용하지 않도록하자

이는 enum 순서를 데이터베이스에 저장하는 것으로 의미 자체도 모호하고 나중에 찾기 힘든 버그를 야기한다.

 

EnumType.STRING으로 하자. 이는 enum의 이름 그 자체를 데이터베이스에 저장한다.

 

 

 


기본키 매핑

직접할당과 자동생성이 있다.

직접 할당시 @Id를 통해 지정할 수 있다.

@Id
private Long id;

 

 

자동할당(@GenerateValue)는 4가지 종류가 있다.

  • IDENTITY: 데이터베이스에 위임, MYSQL
  • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
  • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
  • AUTO: 방언에 따라 자동 지정, 기본값
@GeneratedValue(strategy = GenerationType.[여기에전략])
private Long id;

 

 

IDENTITY 전략

  • 기본키 생성을 DB에 위임 (ex. MySQL의 AUTO_INCREMENT)

 

영속성 컨텍스트에 persist할때, 우리는 Id와 Value를 알고있어야 한다.

 

지난 포스팅의 사진

하지만 DB에서 알아서 기본키를 생성하는데 코드를 짜는 우리가 이를 알 수는 없다. 그럼 어떻게 하느냐?

IDENTITY전략을 사용하면 트랜잭션 커밋 시점이 아닌 persist시점에 INSERT SQL이 실행된다.

그렇게해서 DB에서 식별자를 바로 조회할 수 있다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

 

 

 

SEQUENCE 전략

  • 데이터베이스의 시퀀스 오브젝트 사용
  • 이는 유한한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.
  • 오라클, PostgreSQL, DB2, H2에서 사용

이 전략 또한 DB에서 기본키 값을 부여해 주는 방식이므로 우리는 다음 기본키로 어떤 값이 올지 모른다.

즉, 이 전략도 영속성 컨텍스트에 영속화 시키기 전에 Id값을 미리 조회하는 추가적인 절차가 필요한 것이다.

 

SEQUENCE 전략에서는 persist하는 순간

이와 같이 DB로부터 다음 기본키 값을 받아온다.

 

 

 

 

그런데, 이렇게 INSERT 쿼리를 날리기 위해 추가적인 DB와의 통신이 늘어날수록 성능이 떨어지게 된다. 당연히도.

그렇다면 어떻게 개선할 수 있을까?

 

 

 

SeqenceGeneratorallocationSize 옵션을 통해 개선이 가능하다.

이 옵션은 DB와 한번 통신 할때마다 시퀀스를 한번에 가져와놓을 수량을 지정할 수 있다. 예를들어 50이라고 해두면 50번에 한번씩만 시퀀스를 호출하는 작업을 해주면 된다.

 

@Entity
@SequenceGenerator(
    name = “MEMBER_SEQ_GENERATOR",
    sequenceName = “MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 50)	//한번에 가져올 시퀀스 값 수량
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
	private Long id;

 

 

 

 

TABLE 전략

  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략.
  • 장점은 모든 데이터베이스에 적용 가능하다는 점.
  • 단점은 성능이 다소 떨어진다는 점.

 

create table MY_SEQUENCES (
  sequence_name varchar(255) not null,
  next_val bigint,
  primary key ( sequence_name )
)
@Entity
@SequenceGenerator(
    name = “MEMBER_SEQ_GENERATOR",
    sequenceName = “MY_SEQUENCE", //직접 만든 시퀀스 테이블을 매핑
    pkColunmValue = "MEMBER_SEQ",
    allocationSize = 50)	//한번에 가져올 시퀀스 값 수량
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
	private Long id;

 

 

 

 


권장하는 식별자 전략

  • 기본키 제약조건: null이 아니며 변하면 안된다.
  • 이 조건을 미래까지 쭉 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
  • 권장 형태: Long형 + 대체키 + 키 생성전략 사용

*대체키란,

후보키가 두개 이상일 경우 그 중에서 어느 하나를 기본키로 지정하고 남은 후보키들을 대체키라한다.

 

*후보키란,

 테이블에서 각 행을 유일하게 식별할 수 있는 최소한의 속성들의 집합다.

 

 

 

 

 

 

참고:

더보기

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계  (2) 2021.07.24
[JPA] 연관관계 매핑  (0) 2021.07.21
[JPA] 영속성 관리  (0) 2021.07.17
[JPA] JPA란?  (0) 2021.07.15

이 카테고리는 인프런 김영한님의 JPA 강의를 보고 정리하는 공간입니다.

 


영속성 컨텍스트란?

"엔티티를 영구 저장하는 환경"

 

  • 엄밀히 말하자면 DB에 저장하는 것이 아니라 엔티티를 영속성 컨텍스트라는 곳에 저장한다는 의미
  • 눈에 보이지 않는 논리적인 개념이다.
  • 엔티티 매니저를 통해 접근한다.
  • 엔티티 매니저와 영속성 컨텍스트가 1대 1로 매핑된다

 


엔티티의 생명주기

  • 비영속: 영속성 컨텍스트와 관련 없는 새로운 상태
  • 영속: 영속성 컨텍스트에 관리되는 상태
  • 준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제: 삭제된 상태

 

 

비영속 상태

영속성 컨텍스트와 엔티티가 아무런 관련이 없음. 그저 엔티티가 새로 생성되어있는 상태일 뿐.

Member member = new Member();
member.setId("member1");
member.setUsername("Lee");

그냥 member객체를 생성했을 뿐 아무런 다른 과정이 없다.

 

 

영속 상태

생성한 엔티티를 영속성 컨텍스트에 넣음. 

Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

//객체를 저장한 상태(영속)
em.persist(member);

 

 

준영속, 삭제 상태

준영속 상태는 영속성 컨텍스트에 있다가 detach된 상태

삭제는 삭제 상태

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

//객체를 삭제한 상태(삭제)
em.remove(member);

 

 

 


영속성 컨텍스트의 장점

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

 

1차 캐시

  • find나 persist시 1차 캐시에 저장된다.
  • 영속성 컨텍스트 안에 1차 캐시 공간이 있다. PK가 Key가 되고 엔티티 그 자체가 Value가 된다.
  • JPA를 통한 조회 작업시 바로 DB에 SELECT 쿼리를 날리는 것이 아닌 1차 캐시부터 조회한다. 만약 1차 캐시가 있다면 (찾고자 하는 엔티티가 영속화 돼있다면) 쿼리문을 날리지 않고도 조회가 가능하고 캐시에 없으면 DB를 조회한다.
  • 만약 DB에서 조회를 한다면 그 엔티티는 1차 캐시에 저장된다. 
  • 요청 시작부터 트랜잭션 종료 시까지만 1차 캐시가 사용된다. 매우 짧은 순간이기에 정교한 구조가 아니면 성능상의 큰 이점을 얻기는 힘들다.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//1차 캐시에 저장됨
em.persist(member);

//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
  • member1은 DB에서 조회한 것이 아닌 영속성 컨텍스트의 1차캐시에서 조회한 엔티티이다. 
  • 사실 persist만으로 DB에 저장이 되진 않는다. persist가 완료되면 영속성 컨텍스트에 저장되는 것이지 트랜잭션이 수행되어야 DB에 저장된다.

 

 

만약 1차 캐시에 없는 엔티티를 조회하려고 하면 DB에서 조회 후 1차 캐시에 영속화 한다.

 

 

 

동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true

같은 트랜잭션 안에서의 같은 엔티티를 조회하면 동일한 엔티티임을 보장할 수 있다.

 

 

 

 

트랜잭션을 지원하는 쓰기 지연

em.persist(memberA);
em.persist(memberB);  //여기까지는 DB에 보내지 않고

transaction.commit()  //이 순간에 한번에 SQL이 전송됨

  • memberA와 memberB에 대한 영속화 요청이 들어오면 JPA는 이 엔티티를 분석하여 적절한 SQL문을 생성한다.
  • 단, 바로 DB에 SQL을 전송하는 것이 아닌 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소 라는 곳에 차곡차곡 모아둔다.
  • 그리고 트랜잭션 Commit의 순간 쌓여있던 SQL들이 한번에 요청된다.

 

 

변경 감지(더티체크)

find로 가져와서 setter로 그냥 객체 수정 그냥 하면 JPA가 수정을 감지하고 자동 반영해준다. 즉, 다시 persist 안해도 됨.

 

  • Commit 시점에 수정된 엔티티와 그것의 스냅샷(초기 상태를 가지고 있는 일종의 복사본)을 비교하여 바뀐부분이 있는지 확인하고, 이에대한 SQL문을 SQL저장소에 저장한다. 그리고 update 쿼리를 생성하여 전송한다.
  • 엔티티의 삭제도 이런 원리로 작동한다.

 

 

 


플러시란?

  • 영속성 컨텍스트의 변경내용을 DB에 반영하는 것
  • 모아놓은 SQL들을 한번에 실행(트랜잭션과 다름)
  • 영속성 컨텍스트를 비우지 않음
  • 즉, 영속성 컨텍스트와 DB와의 동기화
  • 플러시를 해도 1차캐시는 유지됨

 

영속성 컨텍스트를 플러시 하는 방법

  1. em.flush() - 직접호출
  2. 트랜잭션 커밋 - 자동호출
  3. JPQL 쿼리 실행 - 자동호출

 

JPQL 쿼리 실행시 플러시가 자동으로 실행된다.

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

왜 자동으로 플러시가 실행되지?

 

persist만으로는 DB까지 반영이 되지 않고 영속성 컨텍스트라는 논리적 공간에만 저장되기에 Commit이 되기 전에 JPQL 쿼리가 요청되면 이에 대한 응답을 해야하기 때문에 현재 영속성 컨텍스트에 있는 것들을 일단 전부 플러시 하는 것. 그래야만 JPQL 쿼리에 대한 응답이 보장된다.

 

 

 


준영속 상태

  • 영속상태였던 엔티티가 영속성 컨텍스트에서 분리된 상태(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함

 

준영속 상태로 만드는 법

  1. em.detach(entity) - 특정 엔티티만 준영속 상태로 전환
  2. em.clear() - 영속성 컨텍스트를 완전히 초기화
  3. em.close() - 영속성 컨텍스트를 종료

 

 

참고:

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계  (2) 2021.07.24
[JPA] 연관관계 매핑  (0) 2021.07.21
[JPA] 엔티티 매핑  (0) 2021.07.19
[JPA] JPA란?  (0) 2021.07.15

이 카테고리는 인프런 김영한님의 JPA 강의를 보고 정리하는 공간입니다.


JPA란?

정의

  • Java Persistence API의 약자로 자바진영의 ORM 기술 표준이다
  • 따지고 보면 JPA는 여러 인터페이스의 모음이다. 그 인터페이스들을 구현한 구현체로 여러가지가 있는데 그 중 가장 대표적인 것이 Hibernate(자바 환경에서 객체-관계 모델 매핑 솔루션)이다. 

 

 

ORM?

  • Object Relational Mapping의 약자로 기존까지의 객체 vs 관계형 데이터베이스 사이의 패러다임을 해결해준다.
  • 객체는 객체대로 설계, RDB는 RDB대로 설계를 하고 이 둘 사이의 매핑을 도와주는 프레임워크라고 할 수 있겠다.

 

동작

JPA는 애플리케이션과 JDBC사이에서 동작한다.

  • 우리가 JPA에게 명령을 내리면 JPA는 적합한 SQL문을 생성하고 JDBC를 사용하여 DB와 상호작용한다. 즉 개발자가 직접 쿼리를 작성하지 않아도 된다! 

 

저장

  • JPA에게 객체를 넘기면 JPA가 객체를 분석하고 INSERT 쿼리를 생성한다.

 

 

조회

  • 조회도 마찬가지다.

 

 

 


왜 JPA를 써야하는가?

  1. SQL 중심 개발이 아닌 객체 중심 개발
  2. 생산성 향상
  3. 유지보수
  4. 패러다임 불일치 해결
  5. 성능
  6. 데이터 접근 추상화와 벤더 독립성
  7. 표준

이라는 장점이 있다.

 

 

 

생산성

JPA는 CRUD가 이미 다 정의돼있다. 너무 간편.

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

특시 수정연산의 경우 자바 컬렉션의 데이터를 다루듯 그냥 set으로 값만 바꿔주면 마법처럼 수정이 이루어진다. 정확한 원리는 추후의 포스팅에서 다루겠다.

 

 

 

유지보수

기존에는 필드에 변경사항이 있을 경우 모든 SQL을 수정해야 했다.

하지만 JPA에선 필드만 수정하면 된다. SQL은 JPA가 알아서 처리해주기때문.

 

 

 

성능

JPA는 애플리케이션과 JDBC 사이에 존재한다. 즉 중간에서 여러 기능(캐싱, 쓰기지연, 즉시로딩, 지연로딩 등)을 수행해 줄 수있기에 성능 최적화에 특화돼있다. 이것들에 대한 자세한 내용 역시 이후의 포스팅에서 다루겠다.

 

 

 

참고:

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계  (2) 2021.07.24
[JPA] 연관관계 매핑  (0) 2021.07.21
[JPA] 엔티티 매핑  (0) 2021.07.19
[JPA] 영속성 관리  (0) 2021.07.17

정처기에서 정규화 문제 틀리고 나서 쓰는 정규화 포스팅. 억울하지만 억울할거 없다. 크윽..

 

 

 

데이터베이스 정규화란?

이상 문제를 해결하기 위해 속성들끼리의 종속관계를 분석하여 여러 릴레이션으로 분해하는 과정.

테이블을 여러개로 분리하는 과정이다보니 속도는 상대적으로 느려질지라도 이상문제들을 방지할 수 있다.

 

 

이상 문제

삽삭생

  • 삽입 이상: 데이터를 저장할 때 원치않는 정보가 함께 삽입되는 경우
  • 삭제 이상: 튜플을 삭제하면서 유지돼야하는 정보까지도 연쇄적으로 삭제되는 경우
  • 갱신 이상: 튜플 중 일부 속성만 갱신시킴으로써 정보의 모순성이 발생하는 경우

 

다음의 테이블에서 예를 들어보자.

 

 

  • 삽입 이상 : 신입 학생이 입학하여 학번과 학년 등을 입력하려 했으나 아직 과목이 정해지지 않았거나, 시험을 보지 않아 성적이 없는 상태이기 때문에 불필요한 정보(과목 이름, 성적)를 함께 삽입 해야 함
  • 삭제 이상 : 학생 번호가 2번인 학생의 과목에 대한 성적을 삭제할 경우 학생 번호와 학년 등 모든 정보가 같이 삭제되어 학생의 정보 자체가 사라짐
  • 갱신 이상 : 학생 번호가 3번인 학생이 2학년이 되어 학년 정보를 변경 하려 하는데 3개를 모두 하나씩 바꿔줘야 함, 하나라도 안바꿀 경우 한명의 학생에 대한 정보가 서로 달라지는 정보의 모순성이 발생

 

 

 

정규화 과정

원부이 결다조

제 1 정규화

도메인은 전부 원자값이어야한다. 원자값이란 유일한 값. 

이 테이블의 핸드폰번호 컬럼은 원자값을 가지고있지 않다. 제 1정규화를 하면 다음과 같은 모습을 가진다.

 

 

 

 

제 2 정규화

2정규화는 부분 함수 종속을 제거한다. 종속성이란 X -> Y 처럼 표현할 수 있다. 이때 X는 결정자, Y는 종속자라고 한다. X가 Y를 결정하고, Y는 X에 종속돼 있다는 의미이다. 즉, 학번을 알면 이름을 알 수있다고 하면 학번이 결정자, 이름이 종속자가 된다.

 

다음 테이블을 보자.

함수(학번, 과목코드)에서 부분 함수인 학번 혼자서 학부와 등록금을 결정할 수 있기 때문에 제2 정규형을 만족할 수 없다. 그러므로 성적, 학부, 등록금에 모두 영향을 주는 학번을 기준으로 릴레이션을 아래와 같이 분리 시킵니다.

 

 

 

 

제 3 정규화

제 3정규화는 이행 함수 종속을 제거한다. 

이 테이블은 학번(X)이 학부(Y)를 결정하고 학부(Y)가 등록음(Z)을 결정하기에 학번(X)이 등록금(Z)을 결정한다고 할 수 있다. 이는 제 3정규형을 만족시키지 못하며 이행 함수 종속을 제거해주어야 한다.

(이행 규칙: X -> Y 이고 Y -> Z면 X -> Z다. 이러면 안됨)

이렇게 분해를 해주면 3정규화가 완료된다.

 

 

그 뒤로는 BCNF(결정자 중 후보키가 아닌것 제거), 4정규화(다치 종속 제거), 5정규화(조인 종속 제거)가 있다만 3정규형까지만 수행해도 충분히 괜찮은 구조라고 한다! 나중에 BCNF 이후로의 필요성이 느껴진다면 다시 포스팅 해보도록 하겠다.

 

 

 

참고:

'DB' 카테고리의 다른 글

[Real MySQL] 인덱스  (1) 2024.10.31
[Typeorm] save() 알차게 사용하기  (1) 2023.04.01

사실 자료구조 카테고리에 맞는 게시글이지만 아직 자료구조 카테고리가 없고 앞으로 딱히 만들 계획이 없기에, 그리고 구현을 자바로 했기에 자바 카테고리에 넣었다! 그냥 그런걸로 하자 ㅎㅎ

 

해시란?

해쉬브라운

  • 해시란 임의의 크기를 가진 데이터를 고정된 크기의 데이터로 변화시켜 저장하는것이다. 이 과정은 해시함수를 통해 진행되며 해시값 자체를 index로 사용하기에 평균 시간복잡도가 O(1)으로 매우 빠르다
  • 키(key) 1개와 값(value) 1개가 1:1로 연관되어 있는 자료구조이다. 따라서 키(key)를 이용하여 값(value)을 도출할 수 있다.
    이 그럼처럼 John Smith라는 이름과 전화번호가 매핑이 되어있고 전화번호를 찾기위해선 John Smith라는 이름을 해시함수를 통해 변환한 해시코드를 통해 찾을 수 있다. 

 

 

해시함수와 충돌

key를 해시함수를 통해서 해시코드로 변환시키고 이 해시코드를 인덱스로 사용하여 value를 저장하는데, 이때 충돌(Collision)이 발생할 수 있다. 다음의 예시를 보자

John Smith와 Sandra Dee라는 key가 해시함수를 통해 해시코드로 변환되었는데 우연히 같은 코드로 변환된 것이다. 

 

즉, 무한한 값(KEY)을 유한한 값(HashCode)으로 표현하면서

서로 다른 두 개 이상의 유한한 값이 동일한 출력 값을 가지게 된다는 것이다.

 

key가 될 수 있는 경우는 무한하고 해시테이블은 유한하니 소위 비둘기집 원리라고 부르는 문제가 발생한다. 이런 문제로 인해 우리는 해시함수의 중요성을 느낄 수 있다. 최대한 겹치지 않고 다양한 값을 보장하는 해시 함수라면 이런 문제를 조금 개선할 수 있지만 그래도 근본적으로는 불가능하다. 따라서 우리는 다른 개선방법을 사용한다. 크게 두가지의 해결 방법이 있는데 Separate Chaining기법과 Open Addressing(개방 주소법)이 있다.

 

 

충돌 해결1. Separate Chaining(Chaining) 기법

John Smith가 들어가 있는데 그 공간에 또 Sandra Dee가 들어갈때 Collision이 발생한다. 이때 Sandra의 value를 기존 John의 뒤에 체인처럼 이어 붙혀준다. 152번지에 John과 Sandra의 정보가 함께 존재하도록 한것이다.

 

장점

  • 한정된 저장 공간을 효율적으로 사용할 수 있다.
  • 해시 함수에 대한 의존성이 상대적으로 적다.
  • 메모리 공간을 미리 잡아 놓을 필요가 없다.(그때그때 이어 붙이기 때문)

단점

  • 한 hash에만 자료가 계속 들어간다면(쏠린다면) 검색 효율이 떨어진다(.최악의 경우 O(n))
  • 외부 저장공간을 사용한다.

 

 

충돌 해결2. Open Addressing(개방주소법)

개방주소법은 데이터의 해시(hash)가 변경되지 않았던 chaining과는 달리 비어있는 해시(hash)를 찾아 데이터를 저장하는 기법이다. 따라서 개방주소법에서의 해시테이블은 1개의 해시와 1개의 값(value)가 매칭되어 있는 형태로 유지된다.

 

 

장점

  • 추가 저장공간이 필요없다

단점

  • 해시 함수의 성능에 전체 해시테이블의 성능이 좌지우지 된다.
  • 데이터의 길이가 늘어나면 그에 해당하는 저장소를 마련해 두어야한다.

 

 

 

Chaining 기법을 사용한 해시테이블 구현

HashTable 클래스

import java.util.LinkedList;

public class HashTable {
	class Node{
		String key;
		String value;
		public Node(String key, String value) {
			this.key = key;
			this.value = value;
		}
		
		String getValue() {
			return value;
		}
		
		void setValue(String value) {
			this.value = value;
		}
	}
	
	//각 배열 칸에 링크드리스트를 넣음으로서 collision이 발생할 시 뒤에 이어나간다.
	LinkedList<Node>[] data;
	
	//해시테이블을 생성하는 순간 생성자를 통해서 배열 크기 초기화
	HashTable(int size){
		this.data = new LinkedList[size];
	}
	
	//키를 해쉬코드로 변환하는 메소드
	int getHashCode(String key) {
		int hashcode = 0;
		for(char c : key.toCharArray()) {
			hashcode += c;
		}
		return hashcode;
	}
	
	//해쉬코드를 배열의 인덱스로 변환하는 메소드
	int convertHashCodeToIndex(int hashcode) {
		return hashcode % data.length;
	}
	
	//배열의 인덱스에 노드가 여러개 있다면 key를 통해 알맞은 value를 찾는 메소드
	Node searchKey(LinkedList<Node> list , String key) {
		//리스트에 아무것도 없으면 null 반환
		if(list == null) {
			 return null;
		}
		
		//리스트에 있는 노드중에 찾는 key를 가진 노드가 있다면 반환
		for(Node node : list) {
			if(node.key.equals(key)) {
				return node;
			}
		}
		
		//리스트에 노드가 없다면 null 반환
		return null;
	}
	
	//key-value를 저장하는 메소드
	void put(String key, String value) {
		int hashcode = getHashCode(key);
		int index = convertHashCodeToIndex(hashcode);
		
		//배열의 해당 인덱스에 들어가있던 리스트 가져온다
		LinkedList<Node> list = data[index];
		
		//배열의 해당 인덱스 번지에 아직 리스트가 없다면
		if(list == null) {
			//리스트 만들고 해당 인덱스에 넣는다
			list = new LinkedList<Node>();
			data[index] = list;
		}
		
		//가져온 리스트에 지금 넣고자하는 key가 먼저 들어가있는지 확인
		Node node = searchKey(list, key);
		
		//노드가 없다면 처음 들어가는 key라는 의미
		if(node == null) {
			list.addLast(new Node(key, value));
		}
		else {
			//이미 해당 key로 들어가있는 노드가 있다면 지금 넣는 key로 덮어쓰기
			node.value = value;
		}
	}
	
	//key를 통해 value 가져오는 메소드
	String get(String key) {
		int hashcode = getHashCode(key);
		int index = convertHashCodeToIndex(hashcode);
		LinkedList<Node> list = data[index];
		
		//해당 인덱스에 있는 list에서 key를 통해 value를 찾는다
		Node node = searchKey(list, key);
		
		//해당 key값의 node가 없으면 Not Found반환, 있으면 value 반환
		return node == null ? "Not Found" : node.value;
	}
}

 

HashTest 클래스

public class HashTest {

	public static void main(String[] args) {
		
		//크기 3의 해쉬테이블 생성
		HashTable ht = new HashTable(3);
		
		ht.put("Lee", "lee is pretty");
		ht.put("Kim", "kim is smart");
		ht.put("Hee", "hee is an angel");
		ht.put("Choi", "choi is cute");
		
		//존재하는 데이터 검색
		System.out.println(ht.get("Lee"));
		System.out.println(ht.get("Kim"));
		System.out.println(ht.get("Hee"));
		System.out.println(ht.get("Choi"));

		//존재하지 않는 데이터 검색
		System.out.println(ht.get("Kang"));
		
		//기존 데이터 덮어쓰기
		ht.put("Choi", "choi is sexy");
		System.out.println(ht.get("Choi"));
	}
}

 

 

데이터는 Node라는 클래스 형태로 저장된다. Node는 key와 value를 가지고 있고 Value의 getter와 setter가 있다.

class Node{
		String key;
		String value;
		public Node(String key, String value) {
			this.key = key;
			this.value = value;
		}
		
		String getValue() {
			return value;
		}
		
		void setValue(String value) {
			this.value = value;
		}
	}

 

 

해시테이블은 배열로 선언하였고 각 칸마다 LinkedList<Node>형으로 선언하여 chaining 기법을 통한 Collision 회피 기법을 선택하였다.

	//각 배열 칸에 링크드리스트를 넣음으로서 collision이 발생할 시 뒤에 이어나간다.
	LinkedList<Node>[] data;

 

 

해시함수는 key의 각 문자들을 유니코드로 반환하여 모두 더하는 방식으로 구성했다. 

인덱스는 해시코드를 해시테이블의 사이즈로 나눈 나머지 값을 사용했다.

	//키를 해쉬코드로 변환하는 메소드
	int getHashCode(String key) {
		int hashcode = 0;
		for(char c : key.toCharArray()) {
			hashcode += c;
		}
		return hashcode;
	}
	
	//해쉬코드를 배열의 인덱스로 변환하는 메소드
	int convertHashCodeToIndex(int hashcode) {
		return hashcode % data.length;
	}

 

 

 

조회를 희망하는 key를 받아서 value를 찾는 메소드이다. key를 받아서 해시함수로 변환 후 인덱스로 변환하여 해당 인덱스에 존재하는 list를 가져온다. 그 리스트에서 우리가 입력한 key를 가진 Node를 찾는 searchKey 메소드를 통해 목적 Node를 찾아낸다.

	//key를 통해 value 가져오는 메소드
	String get(String key) {
		int hashcode = getHashCode(key);
		int index = convertHashCodeToIndex(hashcode);
		LinkedList<Node> list = data[index];
		
		//해당 인덱스에 있는 list에서 key를 통해 value를 찾는다
		Node node = searchKey(list, key);
		
		//해당 key값의 node가 없으면 Not Found반환, 있으면 value 반환
		return node == null ? "Not Found" : node.value;
	}

 

 

searchKey 메소드에서는 우리가 입력한 key를 가진 Node가 존재하는지 확인한다.

	//배열의 인덱스에 노드가 여러개 있다면 key를 통해 알맞은 value를 찾는 메소드
	Node searchKey(LinkedList<Node> list , String key) {
		//리스트에 아무것도 없으면 null 반환
		if(list == null) {
			 return null;
		}
		
		//리스트에 있는 노드중에 찾는 key를 가진 노드가 있다면 반환
		for(Node node : list) {
			if(node.key.equals(key)) {
				return node;
			}
		}
		
		//리스트에 노드가 없다면 null 반환
		return null;
	}

 

 

해시테이블에 데이터를 넣는 메소드로 chaining 기법을 구현했다. 중복되는 key가 이미 존재할 경우 해당 key에대한 value를 덮어쓰는 것으로 구현했다.

	//key-value를 저장하는 메소드
	void put(String key, String value) {
		int hashcode = getHashCode(key);
		int index = convertHashCodeToIndex(hashcode);
		
		//배열의 해당 인덱스에 들어가있던 리스트 가져온다
		LinkedList<Node> list = data[index];
		
		//배열의 해당 인덱스 번지에 아직 리스트가 없다면
		if(list == null) {
			//리스트 만들고 해당 인덱스에 넣는다
			list = new LinkedList<Node>();
			data[index] = list;
		}
		
		//가져온 리스트에 지금 넣고자하는 key가 먼저 들어가있는지 확인
		Node node = searchKey(list, key);
		
		//노드가 없다면 처음 들어가는 key라는 의미
		if(node == null) {
			list.addLast(new Node(key, value));
		}
		else {
			//이미 해당 key로 들어가있는 노드가 있다면 지금 넣는 key로 덮어쓰기
			node.value = value;
		}
	}

 

 

 

참고:

 

정처기 벼락치기하느라 며칠 못올렸다.. 결과는 아직 모르겠다. 너무 간당간당 ㅠㅠ

암튼 이제 다시 한개씩 해보자 후. 오늘은 제네릭에 대한 개념 정리를 해보겠다.

 

제네릭이란?

"제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌

외부에서 사용자에 의해 지정되는 것을 의미"

 

한 줄 요약하면 위와 같다. 말하자면 타입을 매개변수로 넣어주는 그런 느낌.

class Person<T>{
    public T info;
}
 
public class GenericDemo {
 
    public static void main(String[] args) {
        Person<String> p1 = new Person<String>();
        Person<Integer> p2 = new Person<Integer>();
    }
 
}

Person 클래스를 생성할때 <여기>에 타입을 지정해주면 제네릭 변수 T를 통해서 info의 타입이 정해진다. T라는 문자 말고 다른 문자를 써도 되지만 암묵적인 룰이 있다.

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

그러면 이걸 왜 쓰는 것이고 쓰면 뭐가 좋은지 예제를 통해 탐구해보자.

 

 

 

제네릭을 쓰면 좋은점

타입안정성을 확보하고 중복을 줄일 수 있다.

먼저 다음의 중복이 있는 코드를 보자

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class StudentPerson{
    public StudentInfo info;
    StudentPerson(StudentInfo info){ this.info = info; }
}
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class EmployeePerson{
    public EmployeeInfo info;
    EmployeePerson(EmployeeInfo info){ this.info = info; }
}

public class WithoutGeneric {
	public static void main(String[] args) {
	StudentInfo si = new StudentInfo(2);
        StudentPerson sp = new StudentPerson(si);
        System.out.println(sp.info.grade); 			// 2
        EmployeeInfo ei = new EmployeeInfo(1);
        EmployeePerson ep = new EmployeePerson(ei);
        System.out.println(ep.info.rank); 			// 1
	}
}

타입안정성이 확보된 코드이지만, StudentPerson 클래스와 EmployeePerson에서 중복이 발생했다. 같은 목적의 클래스이지만 타입이 다르기에 두번 쓴것인데 이를 개선하고자 이 두 클래스를 Person이라는 하나의 클래스로 통일해보자.

 

 

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class Person{
    public Object info;
    Person(Object info){ this.info = info; }
}
public class GenericDemo {
    public static void main(String[] args) {
        Person p1 = new Person("부장");
        EmployeeInfo ei = (EmployeeInfo)p1.info;
        System.out.println(ei.rank);
    }
}

모든 타입을 받을 수 있는 Object형으로 info를 선언함으로 중복을 줄였다. 

그리고 EmployeeInfo ei = (EmployeeInfo)p1.info 으로 EmployeeInfo의 객체를 생성하려고 했다. 이때 컴파일에서 잡히지 않던 에러가 발생한다. 바로 EmployeeInfo의 멤버변수 rank는 int형인데 여기에 "부장"이라는 String을 넣으려고 한다는 에러다. 이번에는 중복을 줄였지만 타입안정성을 확보하지 못한 모습을 볼 수 있다.

 

이제 제네릭을 적용해서 중복과 타입 안정성을 모두 챙겨보자.

class StudentInfo{
    public int grade;
    StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T>{
    public T info;
    Person(T info){ this.info = info; }
}

public class WithGeneric {

	public static void main(String[] args) {
	Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
        EmployeeInfo ei1 = p1.info;
        System.out.println(ei1.rank); // 성공
         
        Person<String> p2 = new Person<String>("부장");
        String ei2 = p2.info;
        System.out.println(ei2.rank); // 컴파일 실패
	}
}

이 경우에는 맨 마지막줄에서 빨간줄이 뜨면서 컴파일 에러가 발생한다. 즉 중요한것은

  • 런타임이 아닌 컴파일 단계에서 오류가 검출된다.
  • 중복의 제거와 타입 안정성을 동시에 추구할 수 있다.

 

 

제네릭의 특성

1. 복수의 제네릭도 가능하다

class Person<T, S>{
    public T info;
    public S id;
    Person(T info, S id){ 
        this.info = info; 
        this.id = id;
    }
}

 

2. 기본타입은 안되고 참조타입만 사용할 수 있다.

 

 

제네릭의 제한

제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.

abstract class Info{
    public abstract int getLevel();
}
class EmployeeInfo extends Info{
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
    public int getLevel(){
        return this.rank;
    }
}
class Person<T extends Info>{
    public T info;
    Person(T info){ this.info = info; }
}
public class GenericDemo {
    public static void main(String[] args) {
        Person p1 = new Person(new EmployeeInfo(1));
        Person<String> p2 = new Person<String>("부장");
    }
}

class Person<T extends Info>를 보면 T타입은 Info클래스 자신이거나 이것을 상속받는 타입만 가능하다는 의미이다. 상속뿐 아니라 인터페이스의 implements도 가능하다. 반대로 부모타입만 가능하다는 의미의 super도 가능하다!

 

 

참고:

'Java' 카테고리의 다른 글

[Java] Comparable과 Comparator로 객체 정렬하기  (1) 2022.09.12
[Java] 해시/해시테이블이란?  (0) 2021.07.12
[Java] Garbage Collection  (4) 2021.07.01
[Java] Thread/MultiThread 4 - 동시성 문제  (0) 2021.06.29
[Java] Static 키워드  (0) 2021.06.28

이번 포스팅은 네이버 D2 유튜브에 올라온 "그런 REST API로 괜찮은가" 영상을 토대로 작성하였다.

 

 

REST(REpresentational State Transfer) API란

REST API는 REST 아키텍처를 따르는 API이다. 문장을 하나씩 뜯어보자

  • REST → 분산 하이퍼미디어 시스템(ex. 웹)을 위한 아키텍처 스타일
  • 아키텍처 스타일 → 제약 조건의 집합
  • API → API는 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스

 

즉, REST에서 정의한 제약 조건을 모두 지켜야 REST를 따른다고 말할 수 있다는 것이다.

 

 

REST의 장단점

장점

1. Easy to use

REST API의 가장 큰 장점이라고 할 수 있다. 단순히 REST API 메시지를 읽는 것 만으로도 메시지가 의도하는 바를 명확하게 파악할 수 있다. 굳이 해당 메시지의 기능이 무엇인지 알기 위해 메뉴얼을 하나씩 읽어 볼 필요가 없게 만들어 준다.

 

HTTP 인프라를 그대로 사용하기 때문에, REST API 사용을 위한 별도의 인프라 구축을 요구하지 않는다. 그리고 Stateless한 특징 때문에 수행 문맥(Execution Context)가 독립적으로 진행됨으로써 이전에 서버(호스트)에서 진행된 내용들에 대해 클라이언트가 알 필요가 없으며, 이제까지 진행된 히스토리에 대해서도 알 필요가 없게 된다. 즉 해당 URI와 원하는 메소드 자체만 독립적으로 이해하면 된다.


2. Complete Seperation between Client and Server

클라이언트는 REST API를 이용하여 서버와 정보를 주고 받는다. 위에서 언급한 Stateless 한 특징에 따라, 서버는 클라이언트의 문맥을 유지할 필요가 없게 된다. 결국 클라이언트와 서버는 서로 신경쓰지 않으며 동작하게 된다. 서로에게 무관심한 이기적인 상황인 것이다. 하지만 실제로는 각자의 역할이 명확하게 분리되어 있다는 의미로 보는게 더 맞다. 

 

이러한 장점으로 인해 플랫폼의 독립성 확장이라는 효과를 가져오고 HTTP 프로토콜만 지켜진다면 다양한 플랫폼에서 원하는 서비스를 쉽고 빠르게 개발/배포할 수 있게 된다.

 

3. Detail expression for specific data type

REST API는 헤더 부분에 URI 처리 메소드를 명시함으로써, 필요한 실제 데이터를 페이로드(바디)에 표현할 수 있도록 구성할 수 있는 기능을 제공한다. 이는 특정 메소드의 세부적인 표현 문구를 JSON, XML 등 다양한 언어를 이용하여 작성할 수 있다는 장점 뿐만 아니라, 간결한 헤더 표현을 통한 가독성 향상이라는 두마리 토끼를 잡는 효과를 가져다 주게 된다.

 

 

단점

1. Restriction of HTTP MethodREST

API는 HTTP 메소드를 사용하여 URI를 표현한다. 이러한 표현 방법은 다양한 인프라에서도 편리하게 사용할 수 있다는 장점을 주지만, 또 한편으로는 메소드 형태가 제한적 이라는 문제점을 가져오기도 한다.


2. Absence of Standard (표준의 부재)

REST API의 가장 큰 단점이라고 할 수 있는데, 바로 표준이 존재하지 않는다는 것이다.

이는 관리의 어려움과 좋은(공식화 된) API 디자인 가이드가 존재하지 않음을 의미하는데, 결국 REST API는 많은 사람들이 하나씩 쌓아올리는 ‘정당화 된 약속들’ 로 구성되고 움직이게 된다.

 

 

REST를 구성하는 스타일

  1. Client-Server
  2. Stateless
  3. Cache
  4. Uniform Interface
  5. Layered System
  6. Code-on-Demand (optional)

대체로 REST라고 부르는 것들은 위의 조건을 대부분 지키고 있다. 왜냐하면 HTTP만 잘 따라도 Client-Server, Stateless, Cache, Layered System은 다 지킬 수 있기 때문이다. Code-on-Demand는 서버에서 코드를 클라이언트로 보내서 실행할 수 있어야 한다는 것을 의미, 즉 자바스크립트를 의미한다. 이는 필수는 아니다.

 

단, 4번의 Uniform Interface는 잘 지켜지지 않는다고 한다. 

 

 

Uniform Interface 제약 조건

  • Identification of resources
  • Manipulation of resources through representations
  • Self-descriptive messages
  • Hypermedia as the engine of application state(HATEOAS)

Identification of resources은 URI로 리소스가 식별되면 된다는 것이고, Manipulation of resources through representations는 representation 전송을 통해서 리소스를 조작해야된댜는 것이다. 즉, 리소스를 만들거나 삭제, 수정할 때 http 메시지에 그 표현을 전송해야된다는 것이다. 위 2가지 조건은 대부분 잘 지켜지고 있다. 하지만 문제는 아래 2개이다. 이 2가지는 사실 우리가 REST API라고 부르는 거의 모든 것들은 지키지 못하고 있다.

 

Uniform Interface의 세번째, 네번째 조건에 대해 알아보자

 

 

세번째 조건. Self-descriptive messages

Self-descriptive message라는 것은 메시지를 봤을 때 메시지의 내용으로 온전히 해석이 다 가능해야된다는 것이다.

예를 들어 아래와 같은 메시지가 있다고 해보자

GET / HTTP/1.1

 

 

단순히 루트를 얻어오는 GET 요청이다. HTTP 요청 메시지는 목적지가 빠져있어서 Self-descriptive하지 못하다. 다음과 같이 수정할 수 있겠다.

GET / HTTp/1.1
Host: www.example.org

 

 

또 이런 것도 생각해볼 수 있다. 200 응답 메시지이며, JSON 본문이 있다.

HTTP/1.1 200 OK
[ { "op": "remove", "path": "/a/b/c" } ]

 

 

이것도 Self-descriptive 하지 않는데, 그 이유는 이걸 클라이언트가 해석하려고 하면, 어떤 문법으로 작성된 것인지 모르기 때문에 해석에 실패한다. 그렇기 때문에 Content-Type 헤더가 반드시 들어가야한다.

HTTP/1.1 200 OK
Content-Type: application/json
[ { "op": "remove", "path": "/a/b/c" } ]

 

 

 

Content-Type 헤더에서 대괄호, 중괄호, 큰따옴표의 의미가 뭔지 알게 되어, 파싱이 가능하여 문법을 해석할 수 있게 된다. 하지만 여전히 문제가 있다. op 값은 무슨 뜻이고, path가 무엇을 의미하는지는 알 수 없다. 

HTTP/1.1 200 OK
Content-Type: application/json-patch+json
[ { "op": "remove", "path": "/a/b/c" } ]

이렇게 명시를 하면 완전해진다. 이 응답은 json-patch+json이라는 미디어 타입으로 정의된 메시지이기 때문에 json-patch라는 명세를 찾아가서 이해한 다음, 이 메시지를 해석을 하면 그제서야 올바르게 메시지의 의미를 이해할 수 있게 된다.

 

 

 

 

네번째 조건. HATEOAS

REST API의 완성도를 나타내는 RMM지표를 보면 3단계의 조건으로 HATEOAS를 확인할 수 있다.

 

"애플리케이션의 상태는 Hyperlink를 이용해 전이되어야 한다."

 

HateoasREST Api를 사용하는 클라이언트가 전적으로 서버와 동적인 상호작용이 가능하도록 하는 것을 의미한다. 이러한 방법은 클라이언트가 서버로부터 어떠한 요청을 할 때, 요청에 필요한 URI를 응답에 포함시켜 반환하는 것으로 구현 가능하다.

 

 

이런 웹사이트가 있다고 해보자.

루트 홈페이지 → 글 목록 보기 GET → 글 쓰기 GET → 글 저장 POST → 생성된 글 보기 GET → 목록 보기 GET → 반복

이렇게 상태를 전이하는 것을 애플리케이션 상태 전이라고 하고, 이 상태 전이마다 항상 해당 페이지에 있던 링크를 따라가면서 전이했기 때문에 HATEOAS라고 할 수 있다. 말 그대로, 하이퍼 링크를 통한 전이가 되는 것이다.

 

HTTP/1.1 200 OK
Content-Type: text/html

<html>
<head> </head>
<body> <a href="/test"> test </a> </body>
</html>

이처럼 html은 하이퍼링크로 다음 상태로의 전이가 가능하기때문에 HATEOAS하다고 할 수 있다.

JSON의 경우에도 HATEOAS로 표현 가능하다.

 

 

{
    "account_id" : 12345,
    "balance" : "350,000"
}

이러한 JSON 표현이 있다고 했을때 이를 HATEOAS로 바꾸면

 

 

{
    "account_id" : 12345,
    "balance" : "350,000"
    "links" : {
    	{
            "rel" : "self",
            "href" : "http://localhost:8080/accounts/12345"
        },{
            "rel" : "withdraw",
            "href" : "http://localhost:8080/accounts/12345/withdraw"
        },{
            "rel" : "transfer",
            "href" : "http://localhost:8080/accounts/12345/transfer"
        }
    }
}

이렇게 표현될 수 있다.

 

 

HATEOAS를 사용함으로써 생기는 이점을 예를 들어 설명해보자면,

클라이언트가 GET 메소드로 URI 주소 '/member/1'를 호출한다면 사용자 ID1'사용자 정보'를 갖고온다고 가정해보자. 이때 동일한 URIDELETE 메소드를 호출 할 경우 사용자 삭제가 가능하고, PUT 메소드를 호출할 경우 업데이트라고 한다면 일일히 클라이언트 쪽에 알려줘야 한다는 번거로움이 발생한다. 이것을 줄이고자 호출한 URI로부터 연관된 REST API 주소 정보들을 함께 보내주는 역할을 하는 것이 HATEOAS 이다. 그럼으로써 클라이언트는 서버와 상호 작용하는 방법에 대한 사전 지식이 거의 또는 전혀 필요없이 사용 할 수 있게 되는 것이다.

 

 

 

 

 

 

참고:

더보기

'Web' 카테고리의 다른 글

[Web] JWT란?  (0) 2021.07.31
[Web] 세션과 쿠키  (0) 2021.07.02

세션과 쿠키를 사용하는 이유

HTTP(Hypertext Transfer Protocol)는 인터넷상에서 데이터를 주고 받기 위해 서버/클라이언트 모델을 따르는 통신규약이다. 이 HTTP 프로토콜에는 비연결성(Connectionless)과 비상태성(Stateless)이라는 특징이 있다.

 

 

  • Connectionless 프로토콜 (비연결지향) 
    • 클라이언트가 서버에 요청(Request)을 했을 때, 그 요청에 맞는 응답(Response)을 보낸 후 연결을 끊는 처리방식이다.
    • HTTP 1.1 버전에서 연결을 유지하고, 재활용 하는 기능이 Default 로 추가되었다.
      (keep-alive 값으로 변경 가능)
  • Stateless 프로토콜 (상태정보 유지 안함) 
    • 클라이언트와 첫번째 통신에서 데이터를 주고 받았다 해도, 두번째 통신에서 이전 데이터를 유지하지 않는다.
    • 클라이언트의 상태 정보를 가지지 않는 서버 처리 방식이다.

 

하지만 이로 인해 사용자를 식별할 수 없어서 같은 사용자가 요청을 여러번 하더라도 매번 새로운 사용자로 인식하는 단점이 있다. 이를 해결하기 위해 세션과 쿠키를 사용한다.

 

즉, 클라이언트와 정보 유지를 하기 위해 사용하는 것이 쿠키와 세션이다.

 

 

쿠키(Cookie)

HTTP의 일종으로 사용자가 어떠한 웹 사이트를 방문할 경우,
그 사이트가 사용하고 있는 서버에서 사용자의 컴퓨터에 저장하는 작은 기록 정보 파일이다.

HTTP에서 클라이언트의 상태 정보를 클라이언트의 PC에 저장하였다가
필요시 정보를 참조하거나 재사용할 수 있다.

쿠키의 발급/사용 절차

 

  • 쿠키 특징
    1. 이름, , 만료일(저장 기간 설정), 경로 정보로 구성되어 있다.
    2. 클라이언트에 총 300개의 쿠키를 저장할 수 있다.
    3. 하나의 도메인 당 20개의 쿠키를 가질 수 있다
    4. 하나의 쿠키는 4KB까지 저장 가능하다.

  • 쿠키의 동작 순서
    1. 브라우저에서 웹페이지에 접속한다.
    2. 클라이언트가 요청한 웹페이지를 응답으로 받으면서 HTTP 헤더를 통해 해당 서버에서 제공하는 쿠키 값을 응답으로 준다. (이러면 클라이언트는 해당 쿠키를 저장한다.)
    3. 클라이언트가 웹페이지를 요청한 서버에 재 요청시 받았던 쿠키 정보도 같이 HTTP 헤더에 담아서 요청한다.
    4. 서버는 클라이언트의 요청(Request)에서 쿠키 값을 참고하여 비즈니스 로직을 수행한다. (ex 로그인 상태 유지)

즉, HTTP요청시 서버로부터 쿠키를 발급받고 이후 요청들에 쿠키를 함께 동봉하여 요청한다.

 

  • 사용 예시
    1. 방문했던 사이트에 다시 방문 하였을 때 아이디와 비밀번호 자동 입력
    2. 팝업창을 통해 "오늘 이 창을 다시 보지 않기" 체크

 

쿠키는 사용자가 별도로 요청하지 않아도 브라우저(Client)에서 서버에 요청(Request) 시에 Request Header에 쿠키 값을 넣어 요청한다. (=자동이다.)

 

그렇다고 그 많은 쿠키 값을 굳이 모든 요청에 넣어서 비효율적으로 동작하지는 않는다. 도메인 설정을 통해서 지정한 도메인으로 요청할 때만 쿠키 값이 제공되도록 할 수도 있다.

 

 

 

 

세션(Session)

서버(Server)에 클라이언트의 상태 정보를 저장하는 기술로 논리적인 연결을 세션이라고 한다.

웹 서버에 클라이언트에 대한 정보를 저장하고 클라이언트에게는 클라이언트를 구분할 수 있는 ID를 부여하는데 이것을 세션아이디라 한다.

  • 세션 특징
    1. 세션은 쿠키를 기반하고 있지만, 사용자 정보 파일을 브라우저에 저장하는 쿠키와 달리 세션은 서버 측에서 관리한다.
    2. 서버에서는 클라이언트를 구분하기 위해 Session ID를 부여하며 웹 브라우저가 서버에 접속해서 브라우저를 종료할 때까지 인증상태를 유지한다.
    3. 물론 접속 시간에 제한을 두어 일정 시간 응답이 없다면 정보가 유지되지 않게 설정이 가능하다.
    4. 사용자에 대한 정보를 서버에 두기 때문에 쿠키보다 보안에 좋지만, 사용자가 많아질수록 서버 메모리를 많이 차지하게 된다.
    5. 즉 동접자 수가 많은 웹 사이트인 경우 서버에 과부하를 주게 되므로 성능 저하의 요인이 된다.
    6. 클라이언트가 Request를 보내면, 해당 서버의 엔진이 클라이언트에게 유일한 ID를 부여하는 데 이것이 Session ID다.

  • 세션의 동작 순서
    1. 클라이언트가 서버에 접속 시 Session ID를 발급받는다.
    2. 클라이언트는 Session ID에 대해 쿠키를 사용해서 저장하고 가지고 있다.
    3. 클라리언트는 서버에 요청할 때, 이 쿠키의 Session ID를 서버에 전달해서 사용한다.
    4. 서버는 Session ID를 전달 받아서 별다른 작업없이 Session ID로 Session있는 클라이언트 정보를 가져온다.
    5. 클라이언트 정보를 가지고 서버 요청을 처리하여 클라이언트에게 응답한다.

 

즉. 클라이언트가 가진 쿠키에 존재하는 세션ID와 서버가 가진 세션ID를 비교하여 식별.

 

  • 사용 예시
    1. 방문했던 사이트에 다시 방문 하였을 때 아이디와 비밀번호 자동 입력
    2. 팝업창을 통해 "오늘 이 창을 다시 보지 않기" 체크

세션과 쿠키 활용

 

 

 

쿠키와 세션의 차이

  • 저장 위치
    • 쿠키는 클라이언트(브라우저)에 메모리 또는 파일에 저장하고, 세션은 서버 메모리에 저장된다.
  • 보안
    • 쿠키는 클라이언트 로컬(local)에 저장되기도 하고 특히 파일로 저장되는 경우 탈취, 변조될 위험이 있고, Request/Response에서 스나이핑 당할 위험이 있어 보안이 비교적 취약하다. 반대로 Session은 클라이언트 정보 자체는 서버에 저장되어 있으므로 비교적 안전하다.
  • 라이프 사이클
    • 쿠키는 앞서 설명한 지속 쿠키의 경우에 브라우저를 종료하더라도 저장되어 있을 수 있는 반면에 세션은 서버에서 만료시간/날짜를 정해서 지워버릴 수 있기도 하고 세션 쿠키에 세션 아이디를 정한 경우, 브라우저 종료시 세션아이디가 날아갈 수 있다.
  • 속도
    • 쿠키에 정보가 있기 때문에 쿠키에 정보가 있기 때문에 서버에 요청시 헤더를 바로 참조하면 되므로 속도에서 유리하지만, 세션은 제공받은 세션아이디(Key)를 이용해서 서버에서 다시 데이터를 참조해야하므로 속도가 비교적 느릴 수 있다.

 

 

 

세션을 주로 사용하면 좋은데 왜 굳이 쿠키를 사용할까?

→ 세션은 서버에 데이터를 저장 즉, 서버의 자원을 사용하기 때문에 서버 자원에 한계가 있고 메모리를 사용하다보면 속도 저하도 올 수 있기 때문이다.


세션은 사용자의 수 만큼 서버 메모리를 차지하기 때문에
최근에는 이런 문제들을 보완한 토큰 기반의 인증방식을 사용하는 추세다. 그 중 JWT( JSON Web Token)라는 것이 있다. JWT는 다음에 포스팅해보도록 하겠다.

 

 

 

 

참고:

더보기

 

'Web' 카테고리의 다른 글

[Web] JWT란?  (0) 2021.07.31
[Web] REST API  (0) 2021.07.03

+ Recent posts