스파르타 Java 단기 심화 과정


코드카타


프로그래머스 135808 과일 장수

https://school.programmers.co.kr/learn/courses/30/lessons/135808

— 문제 설명

과일 장수가 사과 상자를 포장하고 있습니다. 사과는 상태에 따라 1점부터 k점까지의 점수로 분류하며, k점이 최상품의 사과이고 1점이 최하품의 사과입니다. 사과 한 상자의 가격은 다음과 같이 결정됩니다.

  • 한 상자에 사과를 m개씩 담아 포장합니다.
  • 상자에 담긴 사과 중 가장 낮은 점수가 p (1 ≤ p ≤ k)점인 경우, 사과 한 상자의 가격은 p * m 입니다.

과일 장수가 가능한 많은 사과를 팔았을 때, 얻을 수 있는 최대 이익을 계산하고자 합니다.(사과는 상자 단위로만 판매하며, 남는 사과는 버립니다)

예를 들어, k = 3, m = 4, 사과 7개의 점수가 [1, 2, 3, 1, 2, 3, 1]이라면, 다음과 같이 [2, 3, 2, 3]으로 구성된 사과 상자 1개를 만들어 판매하여 최대 이익을 얻을 수 있습니다.

  • (최저 사과 점수) x (한 상자에 담긴 사과 개수) x (상자의 개수) = 2 x 4 x 1 = 8

사과의 최대 점수 k, 한 상자에 들어가는 사과의 수 m, 사과들의 점수 score가 주어졌을 때, 과일 장수가 얻을 수 있는 최대 이익을 return하는 solution 함수를 완성해주세요.

— 제한 조건

  • 3 ≤ k ≤ 9
  • 3 ≤ m ≤ 10
  • 7 ≤ score의 길이 ≤ 1,000,000
    • 1 ≤ score[i] ≤ k
  • 이익이 발생하지 않는 경우에는 0을 return 해주세요.

— 입출력 예

kmscoreresult
34[1, 2, 3, 1, 2, 3, 1]8
43[4, 1, 2, 2, 4, 4, 4, 4, 1, 2, 4, 2]33

입출력 예 #1

  • 문제의 예시와 같습니다.

입출력 예 #2

  • 다음과 같이 사과 상자를 포장하여 모두 팔면 최대 이익을 낼 수 있습니다.
사과 상자가격
[1, 1, 2]1 x 3 = 3
[2, 2, 2]2 x 3 = 6
[4, 4, 4]4 x 3 = 12
[4, 4, 4]4 x 3 = 12

따라서 (1 x 3 x 1) + (2 x 3 x 1) + (4 x 3 x 2) = 33을 return합니다.

— 문제 풀이

import java.util.*;

class Solution {
    public int solution(int k, int m, int[] score) {
        int answer = 0;
        
        Arrays.sort(score);
        for(int i=score.length-m;i>=0;i-=m){
            answer += score[i] * m;
        }
        
        return answer;
    }
}

인메모리 저장소 및 캐싱 전략 강의

HttpSession

  • Http 요청에는 상태 X(Stateless). 즉, 서버는 사용자가 몇번째 요청한 것인지 등의 정보는 저장 X
  • Session 정보를 브라우저에 전달하고, 브라우저가 Http요청 시 해당 정보를 담아서 보내면 서버는 이전 요청을 보낸 사용자를 기억할 수 있음
  • 사용자를 기억하는 상태를 유지하는 것을 Session이라고 부름

HttpSession 사용 예시

  • SessionController
    @RestController
    public class SessionController {
    
        @GetMapping("/set")
        public String set(@RequestParam("q") String q, HttpSession session) {
            session.setAttribute("q", q);
            return "Saved:" + q;
        }
    
        @GetMapping("/get")
        public String get(HttpSession session){
            return session.getAttribute("q").toString();
        }
    }
    
  • SessionController의 API 요청 시 Tomcat에서 JSESSIONID를 발급해주고, 이를 통해 Tomcat이 세션을 관리함
  • JSESSIONID를 제거할 경우 Tomcat이 이전에 연결된 세션의 정보를 찾지 못하게 됨
  • 서버가 여러개일 경우
    • 다른 포트의 서버에 연결된 데이터를 찾을 수 없음
    • 세션 관리 방안 마련 필요

Sticky Session

  • 특정 사람이 보낸 요청을 하나의 서버로 고정하는 방안
  • 로드밸런서를 통해 요청을 보낸 사용자를 기록, 해당 사용자가 다시 요청할 경우 최초 요청이 전달된 서버로 요청을 전달하는 방식
  • 특정 사용자만 활동을 계속한다면, 요청이 균등하게 분산되지 않음

Session Clustering

  • 여러 서버들이 하나의 저장소를 공유하고, 해당 저장소에 세션에 대한 정보를 저장하는 방안
  • 요청이 어느 서버로 전달이 되든 저장소에 저장된 세션 정보를 확인하고 세션이 유지될 수 있게 함

Sticky Session과 Session Clustering의 장단점

Sticky SessionSession Clustering
장점애플리케이션 입장에선 구현이 쉬움
외부와 통신할 필요가 없음Load Balancer에서 균등하게 요청 분산 가능
서버의 추가 제거가 비교적 자유로움
단점요청이 분산되지 않아 과부하 가능성 존재
한 서버가 다운되면 그 서버가 관리하는 세션도 삭제외부 저장소라는 관리 포인트 추가
외부와 통신하는 지연이 발생

Session Clustering 실습

  • Spring Boot와 Redis를 활용한 Session Clustering 실습
  • 의존성
    • build.gradle
      dependencies {
      	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
      	implementation 'org.springframework.session:spring-session-data-redis'  // 추가!
      	implementation 'org.springframework.boot:spring-boot-starter-web'
      	compileOnly 'org.projectlombok:lombok'
      	annotationProcessor 'org.projectlombok:lombok'
      	testImplementation 'org.springframework.boot:spring-boot-starter-test'
      	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
      }
  • Spring Session Data Redis 의존성 추가 시 내장 Tomcat 대신 Redis에 세션을 저장
    • 쿠키에 JSESSION 대신 SESSION이 저장됨
    • 기본 설정은 Redis에 직렬화가 되어있지 않기 때문에 다른 형태를 원한다면 설정 필요 ( 메서드 명이 동일해야함 !!! )
      @Configuration
      public class RedisConfig {
          // ...
          @Bean
          public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
              return RedisSerializer.json();
              // return RedisSerializer.java();
          }
      }

리더보드 만들기 실습 with Redis

  • 리더보드 : 실시간 랭킹을 보여주는 기능
  • 상품 구매횟수를 인기의 척도로 두고 인기상품 리더보드 만들기 실습
    • 의존성
      dependencies {
      	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
      	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
      	implementation 'org.springframework.boot:spring-boot-starter-web'
      	compileOnly 'org.projectlombok:lombok'
      	runtimeOnly 'com.h2database:h2'
      	annotationProcessor 'org.projectlombok:lombok'
      	testImplementation 'org.springframework.boot:spring-boot-starter-test'
      	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
      }
    • application.yaml
      spring:
        data:
          redis:
            host: localhost
            port: 6379
            password: "!@34"
        datasource:
          url: jdbc:h2:mem:test;
          driver-class-name: org.h2.Driver
        jpa:
          hibernate:
            ddl-auto: create
          defer-datasource-initialization: true
          show-sql: true
      
        sql:
          init:
            mode: always
      
    • DummyData.sql
      INSERT INTO item (name,description,price)
      VALUES
          ('mouse','purus,',68707),
          ('monitor','ullamcorper',11034),
          ('keyboard','magnis dis parturient',37700),
          ('speaker','faucibus leo, in lobortis',58281),
          ('mouse','at pretium',61395),
          ('monitor','massa rutrum',53854),
          ('keyboard','vulputate dui, nec',10952),
          ('speaker','gravida sagittis.',18103),
          ('mouse','tellus. Nunc lectus',81846),
          ('monitor','sagittis augue,',23507);
      INSERT INTO item (name,description,price)
      VALUES
          ('keyboard','ultricies ornare, elit elit',25511),
          ('speaker','est ac',19597),
          ('mouse','ut nisi',13688),
          ('monitor','natoque penatibus et',62116),
          ('keyboard','vulputate mauris',25028),
          ('speaker','Quisque ac libero nec',22685),
          ('mouse','lobortis ultrices. Vivamus',32101),
          ('monitor','arcu. Morbi sit',56267),
          ('keyboard','Mauris vel',48496),
          ('speaker','in',70633);
      INSERT INTO item (name,description,price)
      VALUES
          ('mouse','neque. Nullam',32901),
          ('monitor','eu tellus. Phasellus',35059),
          ('keyboard','posuere cubilia Curae Phasellus',22529),
          ('speaker','Nunc commodo auctor',94930),
          ('mouse','risus. Donec egestas. Aliquam',16251),
          ('monitor','et malesuada fames',60813),
          ('keyboard','justo.',33390),
          ('speaker','sem mollis',56080),
          ('mouse','sit',19070),
          ('monitor','In at pede.',33048);
      INSERT INTO item (name,description,price)
      VALUES
          ('keyboard','vulputate, posuere vulputate, lacus.',45197),
          ('speaker','nulla ante,',96083),
          ('mouse','mauris. Morbi',70104),
          ('monitor','malesuada id, erat. Etiam',55592),
          ('keyboard','facilisis facilisis,',89730),
          ('speaker','posuere',71030),
          ('mouse','nec',26254),
          ('monitor','neque. Nullam ut',61081),
          ('keyboard','scelerisque neque.',77121),
          ('speaker','congue, elit sed',47105);
      
      UPDATE item
      SET price = (price / 1000) * 1000;
    • Item entity
      @Entity
      @Getter
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      public class Item {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
          @Setter
          private String name;
          @Setter
          private String description;
          @Setter
          private Integer price;
      
          @OneToMany(mappedBy = "item", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
          private final List<ItemOrder> orders = new ArrayList<>();
      }
    • ItemOrder entity
      @Getter
      @Entity
      @Table(name = "orders")
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      public class ItemOrder {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          @Setter
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "item_id")
          private Item item;
          private Integer count;
      }
      
    • ItemDto
      @Getter
      @ToString
      @Builder
      @NoArgsConstructor
      @AllArgsConstructor
      public class ItemDto {
          private Long id;
          private String name;
          private String description;
          private Integer price;
      
          public static ItemDto fromEntity(Item entity) {
              return ItemDto.builder()
                      .id(entity.getId())
                      .name(entity.getName())
                      .description(entity.getDescription())
                      .price(entity.getPrice())
                      .build();
          }
      }
    • RedisConfig
      @Configuration
      public class RedisConfig {
      
          @Bean
          public RedisTemplate<String, ItemDto> rankTemplate(
                  RedisConnectionFactory redisConnectionFactory
          ){
              RedisTemplate<String, ItemDto> template = new RedisTemplate<>();
              template.setConnectionFactory(redisConnectionFactory);
              template.setKeySerializer(RedisSerializer.string());
              template.setValueSerializer(RedisSerializer.json());
              return template;
          }
      }
      
    • Item, ItemOrder Repository
      public interface ItemRepository extends JpaRepository<Item, Long> { }
      public interface OrderRepository extends JpaRepository<ItemOrder, Long> { }
    • ItemService
      @Slf4j
      @Service
      public class ItemService {
          private final ItemRepository itemRepository;
          private final OrderRepository orderRepository;
          private final ZSetOperations<String, ItemDto> rankOps;
      
          public ItemService(
                  ItemRepository itemRepository,
                  OrderRepository orderRepository,
                  RedisTemplate<String,ItemDto> rankTemplate
          ) {
              this.itemRepository = itemRepository;
              this.orderRepository = orderRepository;
              this.rankOps = rankTemplate.opsForZSet();
          }
      
          public void purchase(Long id) {
              Item item = itemRepository.findById(id)
                      .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
              orderRepository.save(ItemOrder.builder()
                      .item(item)
                      .count(1)
                      .build());
              rankOps.incrementScore("soldRanks", ItemDto.fromEntity(item), 1);
          }
      
          public List<ItemDto> getMostSold(){
              Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
              if(ranks.isEmpty()){
                  return Collections.emptyList();
              }
              return ranks.stream().toList();
          }
      
      }
    • ItemController
      @RestController
      @RequestMapping("items")
      @RequiredArgsConstructor
      public class ItemController {
          private final ItemService itemService;
      
          @PostMapping("{id}/purchase")
          @ResponseStatus(HttpStatus.NO_CONTENT)
          public void purchase(
                  @PathVariable("id")
                  Long id
          ) {
              itemService.purchase(id);
          }
      
          @GetMapping("/ranks")
          public List<ItemDto> getRanks(){
              return itemService.getMostSold();
          }
      }

캐싱 이란?

  • Cache는 본래 CPU 내부의 작은 영역으로, 빈번히 접근하게 되는 데이터를 저장해두는 임시 기억장치
  • 이처럼 캐시의 목적과 방식을 개발에 적용해, 비번하게 접근하게 되는 데이터를 Redis와 같은 인메모리 DB에 저장함으로써 데이터를 조회하는데 걸리는 시간과 들어가는 자원을 감소시키는 기술을 캐싱이라고 함

캐싱 기본 용어

  • Cache Hit : 캐시에 접근했을 때 찾고 있는 데이터가 있는 경우
  • Cache Miss : 캐시에 접근했을 때 찾고 있는 데이터가 없는 경우
  • Eviction Policy : 캐시에 공간이 부족할 때 어떻게 공간을 확보할 지
  • Caching Strategy : 언제 캐시에 데이터를 저장하고, 언제 캐시를 확인하는지에 대한 전략

캐싱 전략 ( Caching Strategy )

  • Cache-Aside
    • Lazy Loading이라고도 하며, 데이터를 조회할 때 항상 캐시를 먼저 확인하는 전략
    • 캐시에 데이터가 있으면 캐시에서 데이터를, 없으면 원본에서 데이터를 가져온 뒤 캐시에 저장
    • 필요한 데이터만 캐시에 보관
    • 최초 조회 시 캐시를 확인하기 때문에 최초 요청은 상대적으로 오래 걸림
    • 반드시 원본을 확인하지 않기 때문에, 데이터가 최신이라는 보장 X
  • Write-Through
    • 데이터를 작성할 때 항상 캐시에 작성하고, 원본에도 작성
    • 캐시의 데이터 상태는 항상 최신 데이터임이 보장됨
    • 자주 사용하지 않는 데이터도 캐시에 중복해서 작성하기 때문에, 시간이 오래 걸림
  • Write-Behind
    • 캐시에만 데이터를 작성, 일정 주기로 원본을 갱신하는 방식
    • 쓰기가 잦은 상황에 데이터베이스의 부하를 줄일 수 있음
    • 캐시의 데이터가 원본에 적용되기 전 문제가 발생하면 데이터 소실의 위험성이 존재

캐싱 실습 with Spring Boot, Redis

  • 의존성
    • build.gradle
      dependencies {
      	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
      	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
      	implementation 'org.springframework.boot:spring-boot-starter-web'
      	compileOnly 'org.projectlombok:lombok'
      	runtimeOnly 'com.h2database:h2'
      	annotationProcessor 'org.projectlombok:lombok'
      	testImplementation 'org.springframework.boot:spring-boot-starter-test'
      	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
      }
  • application.yaml
    spring:
      data:
        redis:
          host: localhost
          port: 6379
          username: default
          password: systempass
    
      datasource:
        url: jdbc:h2:mem:test;
        driver-class-name: org.h2.Driver
      jpa:
        hibernate:
          ddl-auto: create
        defer-datasource-initialization: true
        show-sql: true
    
      sql:
        init:
          mode: always
  • DummyData.sql
    INSERT INTO item (name,description,price)
    VALUES
        ('mouse','purus,',68707),
        ('monitor','ullamcorper',11034),
        ('keyboard','magnis dis parturient',37700),
        ('speaker','faucibus leo, in lobortis',58281),
        ('mouse','at pretium',61395),
        ('monitor','massa rutrum',53854),
        ('keyboard','vulputate dui, nec',10952),
        ('speaker','gravida sagittis.',18103),
        ('mouse','tellus. Nunc lectus',81846),
        ('monitor','sagittis augue,',23507);
    INSERT INTO item (name,description,price)
    VALUES
        ('keyboard','ultricies ornare, elit elit',25511),
        ('speaker','est ac',19597),
        ('mouse','ut nisi',13688),
        ('monitor','natoque penatibus et',62116),
        ('keyboard','vulputate mauris',25028),
        ('speaker','Quisque ac libero nec',22685),
        ('mouse','lobortis ultrices. Vivamus',32101),
        ('monitor','arcu. Morbi sit',56267),
        ('keyboard','Mauris vel',48496),
        ('speaker','in',70633);
    INSERT INTO item (name,description,price)
    VALUES
        ('mouse','neque. Nullam',32901),
        ('monitor','eu tellus. Phasellus',35059),
        ('keyboard','posuere cubilia Curae Phasellus',22529),
        ('speaker','Nunc commodo auctor',94930),
        ('mouse','risus. Donec egestas. Aliquam',16251),
        ('monitor','et malesuada fames',60813),
        ('keyboard','justo.',33390),
        ('speaker','sem mollis',56080),
        ('mouse','sit',19070),
        ('monitor','In at pede.',33048);
    INSERT INTO item (name,description,price)
    VALUES
        ('keyboard','vulputate, posuere vulputate, lacus.',45197),
        ('speaker','nulla ante,',96083),
        ('mouse','mauris. Morbi',70104),
        ('monitor','malesuada id, erat. Etiam',55592),
        ('keyboard','facilisis facilisis,',89730),
        ('speaker','posuere',71030),
        ('mouse','nec',26254),
        ('monitor','neque. Nullam ut',61081),
        ('keyboard','scelerisque neque.',77121),
        ('speaker','congue, elit sed',47105);
    
    UPDATE item
    SET price = (price / 1000) * 1000;
  • Item Entity
    @Getter
    @Entity
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Item {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        @Setter
        private String name;
        @Setter
        private String description;
        @Setter
        private Integer price;
    }
  • ItemDto
    @Getter
    @ToString
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class ItemDto implements Serializable {
        private Long id;
        private String name;
        private String description;
        private Integer price;
    
        public static ItemDto fromEntity(Item item) {
            return ItemDto.builder()
                    .id(item.getId())
                    .name(item.getName())
                    .description(item.getDescription())
                    .price(item.getPrice())
                    .build();
        }
    }
    
  • ItemRepository
    public interface ItemRepository extends JpaRepository<Item, Long> {
        Page<Item> findAllByNameContains(String name, Pageable pageable);
    }
  • CacheConfig
    @Configuration
    @EnableCaching
    public class CacheConfig {
    
        @Bean
        public RedisCacheManager cacheManager(
                RedisConnectionFactory connectionFactory
        ){
            // 설정 구성
            RedisCacheConfiguration config = RedisCacheConfiguration
                    .defaultCacheConfig()
                    // null을 캐싱하는 지
                    .disableCachingNullValues()
                    // 캐시 유지 시간 ( Time to Live )
                    .entryTtl(Duration.ofSeconds(10))
                    // 캐시를 구분하는 접두사 설정
                    .computePrefixWith(CacheKeyPrefix.simple())
                    // 캐시 값 직렬화 / 역직렬화 설정
                    .serializeValuesWith(
                            SerializationPair.fromSerializer(RedisSerializer.java())
                    );
    
            return RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(config)
                    .build();
        }
    }
    
  • ItemService
    @Slf4j
    @Service
    public class ItemService {
        private final ItemRepository itemRepository;
        public ItemService(ItemRepository itemRepository) {
            this.itemRepository = itemRepository;
        }
    
        @CachePut(cacheNames = "itemCache", key = "#result.id")
        public ItemDto create(ItemDto dto) {
            return ItemDto.fromEntity(itemRepository.save(Item.builder()
                    .name(dto.getName())
                    .description(dto.getDescription())
                    .price(dto.getPrice())
                    .build()));
        }
    
        @Cacheable(cacheNames = "itemAllCache", key = "methodName")
        public List<ItemDto> readAll() {
            return itemRepository.findAll()
                    .stream()
                    .map(ItemDto::fromEntity)
                    .toList();
        }
    
        // cacheNames : 메서드로 인해 만들어질 캐시를 지칭하는 이름
        // key : 캐시 데이터를 구분하기 위해 활용하는 값
        @Cacheable(cacheNames = "itemCache", key = "args[0]")
        public ItemDto readOne(Long id) {
            return itemRepository.findById(id)
                    .map(ItemDto::fromEntity)
                    .orElseThrow(() ->
                            new ResponseStatusException(HttpStatus.NOT_FOUND));
        }
    
        @CachePut(cacheNames = "itemCache", key = "args[0]")
        @CacheEvict(cacheNames = "itemAllCache", allEntries = true)
        public ItemDto update(Long id, ItemDto dto) {
    
            Item item = itemRepository.findById(id)
                    .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
            item.setName(dto.getName());
            item.setDescription(dto.getDescription());
            item.setPrice(dto.getPrice());
            return ItemDto.fromEntity(itemRepository.save(item));
        }
    
        @Caching(
                evict = {
                        @CacheEvict(cacheNames = {"itemAllCache"}, allEntries = true),
                        @CacheEvict(cacheNames = {"itemCache"}, key = "args[0]")
                }
        )
        public void delete(Long id) {
            itemRepository.deleteById(id);
        }
    
        @Cacheable(
                cacheNames = "itemSearchCache",
                key = "{args[0], args[1].pageNumber, args[1].pageSize}"
        )
        public Page<ItemDto> searchByName(String query, Pageable pageable) {
            return itemRepository.findAllByNameContains(query, pageable)
                    .map(ItemDto::fromEntity);
        }
    }
  • ItemController
    @RestController
    @RequestMapping("items")
    @RequiredArgsConstructor
    public class ItemController {
        private final ItemService itemService;
    
        @PostMapping
        public ItemDto create(
                @RequestBody
                ItemDto itemDto
        ) {
            return itemService.create(itemDto);
        }
    
        @GetMapping
        public List<ItemDto> readAll() {
            return itemService.readAll();
        }
    
        @GetMapping("{id}")
        public ItemDto readOne(
                @PathVariable("id")
                Long id
        ) {
            return itemService.readOne(id);
        }
    
        @PutMapping("{id}")
        public ItemDto update(
                @PathVariable("id")
                Long id,
                @RequestBody
                ItemDto dto
        ) {
            return itemService.update(id, dto);
        }
    
        @DeleteMapping("{id}")
        @ResponseStatus(HttpStatus.NO_CONTENT)
        public void delete(
                @PathVariable
                Long id
        ) {
            itemService.delete(id);
        }
    
        @GetMapping("search")
        public Page<ItemDto> search(
                @RequestParam(name = "q", required = true) String query,
                Pageable pageable
        ){
            return itemService.searchByName(query, pageable);
        }
    }
    

인증 / 인가 방식

Session 기반 인증과 JWT 인증 비교

Session 인증 방식

  • 서버에서 인증 정보 저장 후 SessionID를 발급해서 클라이언트에게 제공
  • 클라이언트는 발급 받은 SessionID로 서버에 인증/인가 요청
  • 서버에서는 요청 받은 SessionID로 인증/인가 처리

JWT ( JSON Web Token )

  • 사용자 인증에 필요한 여러 정보들을 Secret Key를 통해 암호화 시킨 JSON 토큰
  • 서버에서 인증된 유저에 대해 인증 정보에 대한 토큰을 발급해서 클라이언트에게 제공
  • 클라이언트는 발급 받은 JWT를 Authorization 헤더에 담아 요청
  • 서버에서는 토큰 위변조 여부와, 만료 시각을 확인하고, 인증 정보를 확인해 사용자를 인가

Session 기반 인증 vs JWT

Session 기반JWT
안정성모든 인증 정보를 서버에서 관리하기 때문에 유리토큰을 발급하고 관리는 하지 않기 때문에, 만료 전까지 대응이 어려움
확장성세션 공유 방안 마련 필요
(ex) Sticky Session, Session Clustering 등)
클라이언트에 토큰을 저장하기 때문에 확장이 자유로움
자원 소비서버에서 인증 정보를 계속 관리해야 하기 때문에, 서버의 부담이 있음토큰만으로 유효성 검증이 가능하므로 자원이 적게 소비되지만 RefreshToken을 활용할 경우 장점이 반감됨
profile
기록을 남겨보자

0개의 댓글