본문 바로가기
프로젝트/토이 프로젝트

[토이 프로젝트] Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 1

by 진꿈청 2024. 2. 3.

 

2024-02-02

 

오늘의 작업을 정리하면,

 

1. ERD 설계

2. Git Repository Create

3. Spring Project 기본 세팅 및 패키지 설계

4. Entity 제작

5. Redis 설치 및 Spring에 Config 세팅이다.

 

첫 번째, ERD 설계

 

https://dbdiagram.io/d/ToyProject-65bc90f1ac844320ae493fa3

 

dbdiagram.io - Database Relationship Diagrams Design Tool

 

dbdiagram.io

 

DB ERD를 설계했다. 우선, 중점적인 Entity를 설계했다.

BaseEntity를 두어 create_at, modified_at를 다른 엔티티가 상속받게 하였다.

또한, 이메일 인증을 위해 User 테이블의 email를 unique로 설정했다.

 

User와 Image 테이블의 관계를 일대다로 하였는데,

프로필 사진만 구현할 예정이였으나 다중 파일 작업도 처리하고 싶어 위와 같이 설정했다.

 

User, Delivery, Order, OrderItem에는 bool값의 deleted를 설정했는데,

그 이유는 soft delete를 한 번 구현해보기 위해서다.

 

두 번째, Git Repository Create


간단하게 ReadMe.md 파일이 있는 main 브랜치를 두고,

develop branch를 열어 Spring Project와 연동하였다.

 

세 번째, Spring Project 기본 세팅 및 패키지 설계

 

Toy Project의 디펜던시는 다음과 같다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.8'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.hanyoonsoo'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'


    //querydsl dependencies 추가(스프링부트 3.0 이상)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    // JUnit5
    testImplementation("org.junit.platform:junit-platform-launcher:1.5.2")
    testImplementation("org.junit.jupiter:junit-jupiter:5.5.2")


    // Redis 추가
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

tasks.named('bootBuildImage') {
    builder = 'paketobuildpacks/builder-jammy-base:latest'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

패키지 설계는

최상위 패키지는 module로 두었고,

하위에 dto, controller, service, repository로 두었다.

또한, constants는 비즈니스 로직이 아닌 부분에서 쓰이는 것들을 위해 두었다.

 

global 패키지 안에는 config(스프링 환경 세팅), exception, security를 두어 구분하였다.

 

패키지 구성

 

 

네 번째, Entity 제작

 

전체 Entity를 보여주는 건 너무 길어지므로 고민했던 부분들이 있는 Order Entity를 예시로 설명한다.

@Entity
@Getter @Setter
@SQLDelete(sql = "UPDATE order SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
@Table(name = "orders")
public class Order extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(value = EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    private boolean deleted = Boolean.FALSE;

 

앞서, 말했던 Soft Delete를 위해 @SQLDelete와 @Where를 추가했다.

 

Soft Delete: 소프트 딜리트를 하게 되면 데이터가 사라지지 않기 때문에 확인이 가능하다. Hard Delete의 경우 데이터를 찾지 못한다.

                      하지만, 장단점이 존재한다.(나중에 좀 더 자세히 다뤄보겠습니다.)

 

@SQLDelete: Entity가 삭제 요청을 받았을 때 삭제 대신 해당 쿼리가 동작한다.

@Where: 쿼리가 전송될 때 항상 clause절에 있는 내용이 포함되어 쿼리가 날라간다.

 

Order Entity에는 cascade가 적용되어 있는데 이 전략을 유지할지는 일단 고민중에 있다.

실제로는 cascade는 너무 위험도가 크기도 하고 실제로도 잘 사용하지 않는다는 말도 있어서,

편리하긴 하지만 사용에 대해 좀 더 고민해봐야 할 것 같다.(이것에 따라 코드가 많이 바뀔 거 같다.)

 

다섯 번째, Redis 설치 및 Spring에 Config 세팅이다.

 

Mac에 Redis를 설치했다. 추후에 Mac에 설치되어 있는 Docker Image로 Redis를 띄울 수도 있다.

 

 

RedisConfig

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {

    private final RedisProperties redisProperties;

    // RedisProperties를 활용하여 yaml에 저장한 host, post 값 사용
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

    // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 함
    @Bean
    public RedisTemplate<String, Object> redisTemplate(){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}

 

RedisService

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    public void setValues(String key, String value){
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        values.set(key, value);
    }

    public void setValues(String key, String value, Duration duration){
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        values.set(key, value, duration);
    }

    @Transactional(readOnly = true)
    public String getValues(String key){
        ValueOperations<String, Object> values = redisTemplate.opsForValue();
        return (values.get(key) == null) ? "false" : (String) values.get(key);
    }

    public void deleteValues(String key){
        redisTemplate.delete(key);
    }

    public void expireValues(String key, int timeout){
        redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
    }

    public void setHashOps(String key, Map<String, String> data){
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        values.putAll(key, data);
    }

    @Transactional(readOnly = true)
    public String getHashOps(String key, String hashKey){
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        return Boolean.TRUE.equals(values.hasKey(key, hashKey)) ? (String) redisTemplate.opsForHash().get(key, hashKey) : "";
    }

    public void deleteHashOps(String key, String hashKey){
        HashOperations<String, Object, Object> values = redisTemplate.opsForHash();
        values.delete(key, hashKey);
    }

    public boolean checkExistsValue(String value){
        return !value.equals("false");
    }
}

 

다음과 같이 Redis Setting을 진행했다. 자세한 내용은 길어져서 Redis에 대해 정리한 글을 다시 한 번 작성할 예정이다.

 

참고 블로그

 

정리

 

2024-02-02에 작업한 내용을 옮겼다. 계속 이걸 하다보니 12시가 다 됐다는 사실도 모르고 진행한 것 같다.

현재 정처기 시험을 준비하고 있지만, 차곡차곡 프로젝트를 진행하기 위해 노력하려 한다.

 

프로젝트를 진행하면서 맘대로 작성하였다. Git Commit 메시지도 정리해서 작성하기 위해 노력하려 한다.

 

화이팅!!!!