Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

캐싱 적용 #27

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ prometheus/config/**.log
prometheus/volume/data/

# grafana
grafana/
grafana/
redis/
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.32'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
18 changes: 16 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ services:
restart: always
expose:
- "8080"

environment:
SPRING_DATASOURCE_URL: jdbc:mysql://my-mysql:3306/mydb
SPRING_DATASOURCE_USERNAME: "root"
SPRING_DATASOURCE_PASSWORD: "123"
depends_on:
- my-mysql

nginx:
image: nginx:latest
volumes:
Expand Down Expand Up @@ -51,6 +51,20 @@ services:
depends_on:
- my-mysql

redis:
image: redis:latest
container_name: redis
ports:
- "6380:6380"
volumes:
- ./redis/data:/data
- ./redis/conf/redis.conf:/usr/local/conf/redis.conf
labels:
- "name=redis"
- "mode=standalone"
restart: always
command: redis-server /usr/local/conf/redis.conf --port 6380

prometheus:
image: prom/prometheus:v2.37.6
container_name: prometheus
Expand All @@ -73,4 +87,4 @@ services:
- "3000:3000"
volumes:
- ./grafana/volume:/var/lib/grafana
restart: always
restart: always
69 changes: 69 additions & 0 deletions src/main/java/system/design/interview/CacheableRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package system.design.interview;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.transaction.annotation.Transactional;
import system.design.interview.domain.BaseEntity;

import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Slf4j
public abstract class CacheableRepository<E extends BaseEntity, ID> {

protected final JpaRepository<E, ID> jpaRepository;
private final HashOperations<String, String, E> redisHashSupport;
private final String prefix;

@Transactional
public E save(E entity) {
E savedEntity = jpaRepository.save(entity);
String key = prefix + savedEntity.getId();
log.info("try to save {" + savedEntity + "} in redis.");
redisHashSupport.put(key, key, savedEntity);
log.info("save ok {" + savedEntity + "} in redis.");
log.info("try to save {" + savedEntity + "} in database.");
return savedEntity;
}

public List<E> findAll() {
return jpaRepository.findAll();
}

@SuppressWarnings("unchecked")
public Optional<E> findById(ID id) {
String key = prefix + id.toString();
E value = redisHashSupport.get(key, key);
if (value == null) {
log.info("{" + key + "} is not exist in redis. try to find data in db...");
Optional<E> entity = jpaRepository.findById(id);
if (entity.isEmpty()) {
log.info("{" + key + "} is not exist in db.");
return Optional.empty();
}
log.info("{" + entity + "} is exist in db. insert in redis and return.");
redisHashSupport.put(key, key, entity.get());
return entity;
}
return Optional.of(value);
}

@Transactional
public void deleteById(ID id) {
String key = prefix + id.toString();
E value = redisHashSupport.get(key, key);
deleteInRedisIfExist(key, value);
log.info("{" + value + "} will delete from db.");
jpaRepository.deleteById(id);
}

protected void deleteInRedisIfExist(String key, E value) {
if (value != null) {
log.info("{" + value + "} is exist in redis. delete from redis.");
redisHashSupport.delete(key, key);
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/system/design/interview/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package system.design.interview.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@RequiredArgsConstructor
public class RedisConfig {

private final RedisProperties redisProperties;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
6 changes: 5 additions & 1 deletion src/main/java/system/design/interview/domain/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;

import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
Expand All @@ -11,9 +13,11 @@
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
public abstract class BaseEntity implements Serializable {

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

public abstract Long getId();
}
16 changes: 4 additions & 12 deletions src/main/java/system/design/interview/domain/team/Team.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
package system.design.interview.domain.team;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import lombok.*;
import system.design.interview.domain.BaseEntity;

@Entity
@Getter
@Entity
@Table(name = "team")
@EqualsAndHashCode(of = {"id"}, callSuper = false)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Team extends BaseEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package system.design.interview.domain.team;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import system.design.interview.CacheableRepository;

import java.util.Optional;

@Repository
public class TeamCacheableRepository extends CacheableRepository<Team, Long> {

private static final String PREFIX = Team.class.getName() + "_";

public TeamCacheableRepository(TeamRepository teamRepository,
RedisTemplate<String, Object> redisTemplate) {
super(teamRepository, redisTemplate.opsForHash(), PREFIX);
}

@Transactional
public Optional<Team> update(Long id, String changedTeamName) {
String key = PREFIX + id.toString();
Optional<Team> entity = jpaRepository.findById(id);
if (entity.isEmpty()) {
return Optional.empty();
}
deleteInRedisIfExist(key, entity.get());

entity.get().update(changedTeamName);

return Optional.of(jpaRepository.save(entity.get()));
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
package system.design.interview.domain.team;

import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import system.design.interview.domain.team.dto.request.TeamCreateRequest;
import system.design.interview.domain.team.dto.request.TeamUpdateRequest;
import system.design.interview.domain.team.dto.response.TeamResponse;

import java.net.URI;
import java.util.List;

@RequiredArgsConstructor
@RequestMapping("/teams")
@RestController
Expand All @@ -35,6 +29,12 @@ public ResponseEntity<List<TeamResponse>> findAll() {
return ResponseEntity.ok(response);
}

@GetMapping("/{id}")
public ResponseEntity<TeamResponse> findById(@PathVariable(name = "id") Long teamId) {
TeamResponse response = TeamResponse.from(teamService.findById(teamId));
return ResponseEntity.ok(response);
}

@PutMapping("/{id}")
public ResponseEntity<Void> updateTeam(
@PathVariable(name = "id") Long teamId,
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/system/design/interview/domain/team/TeamService.java
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
package system.design.interview.domain.team;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import system.design.interview.domain.team.dto.request.TeamCreateRequest;
import system.design.interview.domain.team.dto.request.TeamUpdateRequest;
import system.design.interview.domain.team.dto.response.TeamResponse;

import java.util.List;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class TeamService {

private final TeamRepository teamRepository;
private final TeamCacheableRepository teamCacheableRepository;

@Transactional
public Long createTeam(TeamCreateRequest request) {
Team team = Team.builder()
.name(request.getName())
.build();
Team savedTeam = teamRepository.save(team);
Team savedTeam = teamCacheableRepository.save(team);
return savedTeam.getId();
}

public List<TeamResponse> findAll() {
return teamRepository.findAll()
return teamCacheableRepository.findAll()
.stream()
.map(TeamResponse::from)
.toList();
}

@Transactional
public void updateTeam(Long teamId, TeamUpdateRequest request) {
Team team = teamRepository.findById(teamId)
teamCacheableRepository.update(teamId, request.getName())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 팀입니다."));
team.update(request.getName());
}

@Transactional
public void deleteMemberById(Long teamId) {
teamRepository.deleteById(teamId);
teamCacheableRepository.deleteById(teamId);
}

public Team findById(Long teamId) {
return teamCacheableRepository.findById(teamId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 팀입니다."));
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ spring:
hibernate:
show_sql: true

data:
redis:
host: redis
port: 6380

management:
endpoints:
Expand Down