https://school.programmers.co.kr/learn/courses/30/lessons/135808
— 문제 설명
과일 장수가 사과 상자를 포장하고 있습니다. 사과는 상태에 따라 1점부터 k점까지의 점수로 분류하며, k점이 최상품의 사과이고 1점이 최하품의 사과입니다. 사과 한 상자의 가격은 다음과 같이 결정됩니다.
과일 장수가 가능한 많은 사과를 팔았을 때, 얻을 수 있는 최대 이익을 계산하고자 합니다.(사과는 상자 단위로만 판매하며, 남는 사과는 버립니다)
예를 들어, k
= 3, m
= 4, 사과 7개의 점수가 [1, 2, 3, 1, 2, 3, 1]이라면, 다음과 같이 [2, 3, 2, 3]으로 구성된 사과 상자 1개를 만들어 판매하여 최대 이익을 얻을 수 있습니다.
사과의 최대 점수 k
, 한 상자에 들어가는 사과의 수 m
, 사과들의 점수 score
가 주어졌을 때, 과일 장수가 얻을 수 있는 최대 이익을 return하는 solution 함수를 완성해주세요.
— 제한 조건
k
≤ 9m
≤ 10score
의 길이 ≤ 1,000,000score[i]
≤ k— 입출력 예
k | m | score | result |
---|---|---|---|
3 | 4 | [1, 2, 3, 1, 2, 3, 1] | 8 |
4 | 3 | [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;
}
}
@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();
}
}
Sticky Session | Session Clustering | |
---|---|---|
장점 | 애플리케이션 입장에선 구현이 쉬움 | |
외부와 통신할 필요가 없음 | Load Balancer에서 균등하게 요청 분산 가능 | |
서버의 추가 제거가 비교적 자유로움 | ||
단점 | 요청이 분산되지 않아 과부하 가능성 존재 | |
한 서버가 다운되면 그 서버가 관리하는 세션도 삭제 | 외부 저장소라는 관리 포인트 추가 | |
외부와 통신하는 지연이 발생 |
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'
}
@Configuration
public class RedisConfig {
// ...
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return RedisSerializer.json();
// return RedisSerializer.java();
}
}
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'
}
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
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;
@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<>();
}
@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;
}
@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();
}
}
@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;
}
}
public interface ItemRepository extends JpaRepository<Item, Long> { }
public interface OrderRepository extends JpaRepository<ItemOrder, Long> { }
@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();
}
}
@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();
}
}
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'
}
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
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;
@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;
}
@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();
}
}
public interface ItemRepository extends JpaRepository<Item, Long> {
Page<Item> findAllByNameContains(String name, Pageable pageable);
}
@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();
}
}
@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);
}
}
@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 | |
---|---|---|
안정성 | 모든 인증 정보를 서버에서 관리하기 때문에 유리 | 토큰을 발급하고 관리는 하지 않기 때문에, 만료 전까지 대응이 어려움 |
확장성 | 세션 공유 방안 마련 필요 (ex) Sticky Session, Session Clustering 등) | 클라이언트에 토큰을 저장하기 때문에 확장이 자유로움 |
자원 소비 | 서버에서 인증 정보를 계속 관리해야 하기 때문에, 서버의 부담이 있음 | 토큰만으로 유효성 검증이 가능하므로 자원이 적게 소비되지만 RefreshToken을 활용할 경우 장점이 반감됨 |