From b08bd480418449cc9d2fabc6856142e466f138ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=95=B8=EB=AA=A8?= Date: Tue, 4 Jul 2023 22:06:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20docker-compose=EC=97=90=20redis=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(6380=ED=8F=AC=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- build.gradle | 3 +-- docker-compose.yaml | 18 ++++++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d1cd1f4..a7ca9ce 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ prometheus/config/**.log prometheus/volume/data/ # grafana -grafana/ \ No newline at end of file +grafana/ +redis/ diff --git a/build.gradle b/build.gradle index af77529..8d485a3 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/docker-compose.yaml b/docker-compose.yaml index 97445b7..08738af 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: @@ -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 @@ -73,4 +87,4 @@ services: - "3000:3000" volumes: - ./grafana/volume:/var/lib/grafana - restart: always \ No newline at end of file + restart: always From e8c0304ae06f75ca972af9f74b0c6e287210b49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=95=B8=EB=AA=A8?= Date: Tue, 4 Jul 2023 22:07:00 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20team=20Entity=EC=97=90=20=EC=8B=9C?= =?UTF-8?q?=EB=B2=94=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=BA=90=EC=8B=B1=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design/interview/CacheableRepository.java | 69 +++++++++++++++++++ .../design/interview/config/RedisConfig.java | 34 +++++++++ .../design/interview/domain/BaseEntity.java | 6 +- .../design/interview/domain/team/Team.java | 16 ++--- .../domain/team/TeamCacheableRepository.java | 33 +++++++++ .../interview/domain/team/TeamController.java | 20 +++--- .../interview/domain/team/TeamService.java | 19 +++-- src/main/resources/application.yaml | 4 ++ 8 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 src/main/java/system/design/interview/CacheableRepository.java create mode 100644 src/main/java/system/design/interview/config/RedisConfig.java create mode 100644 src/main/java/system/design/interview/domain/team/TeamCacheableRepository.java diff --git a/src/main/java/system/design/interview/CacheableRepository.java b/src/main/java/system/design/interview/CacheableRepository.java new file mode 100644 index 0000000..d5d2e19 --- /dev/null +++ b/src/main/java/system/design/interview/CacheableRepository.java @@ -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 { + + protected final JpaRepository jpaRepository; + private final HashOperations 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 findAll() { + return jpaRepository.findAll(); + } + + @SuppressWarnings("unchecked") + public Optional 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 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); + } + } +} diff --git a/src/main/java/system/design/interview/config/RedisConfig.java b/src/main/java/system/design/interview/config/RedisConfig.java new file mode 100644 index 0000000..db2cf8a --- /dev/null +++ b/src/main/java/system/design/interview/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/src/main/java/system/design/interview/domain/BaseEntity.java b/src/main/java/system/design/interview/domain/BaseEntity.java index f7ce588..9290958 100644 --- a/src/main/java/system/design/interview/domain/BaseEntity.java +++ b/src/main/java/system/design/interview/domain/BaseEntity.java @@ -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; @@ -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(); } diff --git a/src/main/java/system/design/interview/domain/team/Team.java b/src/main/java/system/design/interview/domain/team/Team.java index e966716..57cc0c7 100644 --- a/src/main/java/system/design/interview/domain/team/Team.java +++ b/src/main/java/system/design/interview/domain/team/Team.java @@ -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 diff --git a/src/main/java/system/design/interview/domain/team/TeamCacheableRepository.java b/src/main/java/system/design/interview/domain/team/TeamCacheableRepository.java new file mode 100644 index 0000000..7a26257 --- /dev/null +++ b/src/main/java/system/design/interview/domain/team/TeamCacheableRepository.java @@ -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 { + + private static final String PREFIX = Team.class.getName() + "_"; + + public TeamCacheableRepository(TeamRepository teamRepository, + RedisTemplate redisTemplate) { + super(teamRepository, redisTemplate.opsForHash(), PREFIX); + } + + @Transactional + public Optional update(Long id, String changedTeamName) { + String key = PREFIX + id.toString(); + Optional 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())); + } +} diff --git a/src/main/java/system/design/interview/domain/team/TeamController.java b/src/main/java/system/design/interview/domain/team/TeamController.java index a47e696..3b5fc33 100644 --- a/src/main/java/system/design/interview/domain/team/TeamController.java +++ b/src/main/java/system/design/interview/domain/team/TeamController.java @@ -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 @@ -35,6 +29,12 @@ public ResponseEntity> findAll() { return ResponseEntity.ok(response); } + @GetMapping("/{id}") + public ResponseEntity findById(@PathVariable(name = "id") Long teamId) { + TeamResponse response = TeamResponse.from(teamService.findById(teamId)); + return ResponseEntity.ok(response); + } + @PutMapping("/{id}") public ResponseEntity updateTeam( @PathVariable(name = "id") Long teamId, diff --git a/src/main/java/system/design/interview/domain/team/TeamService.java b/src/main/java/system/design/interview/domain/team/TeamService.java index e021165..d80e2ca 100644 --- a/src/main/java/system/design/interview/domain/team/TeamService.java +++ b/src/main/java/system/design/interview/domain/team/TeamService.java @@ -1,6 +1,5 @@ package system.design.interview.domain.team; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,24 +7,26 @@ 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 findAll() { - return teamRepository.findAll() + return teamCacheableRepository.findAll() .stream() .map(TeamResponse::from) .toList(); @@ -33,13 +34,17 @@ public List findAll() { @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("존재하지 않는 팀입니다.")); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f31b26b..e6ba28a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,6 +15,10 @@ spring: hibernate: show_sql: true + data: + redis: + host: redis + port: 6380 management: endpoints: