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

[1단계 - DB 복제와 캐시] 알파카(최휘용) 미션 제출합니다. #71

Open
wants to merge 9 commits into
base: slimsha2dy
Choose a base branch
from
68 changes: 68 additions & 0 deletions src/main/java/coupon/DataSourceConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package coupon;

import com.zaxxer.hikari.HikariDataSource;
import jakarta.persistence.EntityManagerFactory;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class DataSourceConfig {

public static final String READER = "reader";
public static final String WRITER = "writer";

@ConfigurationProperties(prefix = "coupon.datasource.writer")
@Bean
public DataSource writerDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

@ConfigurationProperties(prefix = "coupon.datasource.reader")
@Bean
public DataSource readerDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}

@DependsOn({"writerDataSource", "readerDataSource"})
@Bean
public DataSource routingDataSource(
@Qualifier("writerDataSource") DataSource writer,
@Qualifier("readerDataSource") DataSource reader) {
ReadOnlyDataSourceRouter routingDataSource = new ReadOnlyDataSourceRouter();

Map<Object, Object> dataSourceMap = new HashMap<>();

dataSourceMap.put(WRITER, writer);
dataSourceMap.put(READER, reader);

routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(writer);

return routingDataSource;
}

@DependsOn({"routingDataSource"})
@Primary
@Bean
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
return jpaTransactionManager;
}
Comment on lines +63 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 궁금해서 질문드려요~~ 이 설정의 역할은 무엇인가요? 😄

}
15 changes: 15 additions & 0 deletions src/main/java/coupon/ReadOnlyDataSourceRouter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package coupon;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class ReadOnlyDataSourceRouter extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
return DataSourceConfig.READER;
}
return DataSourceConfig.WRITER;
}
}
6 changes: 6 additions & 0 deletions src/main/java/coupon/domain/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package coupon.domain;

public enum Category {

FASHION, HOME_APPLIANCE, FURNITURE, FOOD
}
43 changes: 43 additions & 0 deletions src/main/java/coupon/domain/MemberCoupon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package coupon.domain;

import coupon.domain.coupon.Coupon;
import coupon.domain.member.Member;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberCoupon {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "coupon_id")
private Coupon coupon;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
private boolean used;
private LocalDate issueDate;
private LocalDate expirationDate;

public MemberCoupon(Long id, Coupon coupon, Member member, boolean used, LocalDate issueDate) {
this.id = id;
this.coupon = coupon;
this.member = member;
this.used = used;
this.issueDate = issueDate;
this.expirationDate = issueDate.plusDays(6);
}
}
51 changes: 51 additions & 0 deletions src/main/java/coupon/domain/coupon/Coupon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package coupon.domain.coupon;

import coupon.domain.Category;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Coupon {

private static final int MINIMUM_DISCOUNT_RATE = 3;
private static final int MAXIMUM_DISCOUNT_RATE = 20;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private CouponName couponName;
@Embedded
private DiscountMount discountMount;
@Embedded
private MinimumMount minimumMount;
private Category category;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 enum은 Enumerated 붙여주지 않아도 엔티티에 넣을 수 있군요..!

@Embedded
private Period period;

public Coupon(CouponName couponName, DiscountMount discountMount, MinimumMount minimumMount, Category category,
Period period) {
validateDiscountRate(discountMount, minimumMount);
this.couponName = couponName;
this.discountMount = discountMount;
this.minimumMount = minimumMount;
this.category = category;
this.period = period;
}

private void validateDiscountRate(DiscountMount discountMount, MinimumMount minimumMount) {
float discountRate = (float) discountMount.getDiscountMount() / minimumMount.getMinimumMount() * 100;
int truncatedDiscountRate = (int) discountRate;
if (truncatedDiscountRate < MINIMUM_DISCOUNT_RATE || truncatedDiscountRate > MAXIMUM_DISCOUNT_RATE) {
throw new IllegalArgumentException("쿠폰 할인율은 3% 이상, 20% 이하여야 합니다.");
Comment on lines +44 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiscountMount 내부에서 검증하면 안되나요??

할인율을 검증하는 것 같은데 mount라는 네이밍은 어떤 이유로 결정하셨을까요?

Copy link
Member Author

@slimsha2dy slimsha2dy Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiscountMount는 할인 금액이고 할인율을 검증하기 위해서는 할인 금액 DiscountMount와 최소 주문 금액 MinimumMount가 둘 다 필요합니다.
따라서 할인 금액인 DiscountMount에서 할인율을 검증하는 건 저는 어색하다고 생각하는데요. 할인율은 할인 금액과 최소 주문 금액을 통해서 계산해야 하기 때문에 이를 둘 다 알고 있는 쿠폰에서 검증하는 것이 좀 더 적절하다고 생각합니다!
혹시 제가 놓친 부분이나 다른 의견 있으신가요??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 착각했네요.. 혼란을 줘서 죄송합니다.. discountrate를 별도의 객체로 분리하지 않은 이유가 있으신가요?

}
}
}
27 changes: 27 additions & 0 deletions src/main/java/coupon/domain/coupon/CouponName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package coupon.domain.coupon;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CouponName {

private static final int MAX_LENGTH = 30;

private String couponName;

public CouponName(String couponName) {
validate(couponName);
this.couponName = couponName;
}

private void validate(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("이름은 비어있을 수 없습니다.");
}
if (name.length() > MAX_LENGTH) {
throw new IllegalArgumentException("이름은 최대 " + MAX_LENGTH + "자 입니다.");
}
}
}
32 changes: 32 additions & 0 deletions src/main/java/coupon/domain/coupon/DiscountMount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package coupon.domain.coupon;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class DiscountMount {

private static final int MINIMUM_MOUNT = 1_000;
private static final int MAXIMUM_MOUNT = 10_000;
private static final int UNIT = 500;

private int discountMount;

public DiscountMount(int discountMount) {
validate(discountMount);
this.discountMount = discountMount;
}

private void validate(int mount) {
if (mount < MINIMUM_MOUNT) {
throw new IllegalArgumentException("할인 금액은 " + MINIMUM_MOUNT + "원 이상이어야 합니다.");
}
if (mount > MAXIMUM_MOUNT) {
throw new IllegalArgumentException("할인 금액은 " + MAXIMUM_MOUNT + "원 이하여야 합니다.");
}
if (mount % UNIT != 0) {
throw new IllegalArgumentException("할인 금액은 " + UNIT + "원 단위여야 합니다.");
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/coupon/domain/coupon/MinimumMount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package coupon.domain.coupon;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MinimumMount {

private static final int MINIMUM_MOUNT = 5_000;
private static final int MAXIMUM_MOUNT = 100_000;

private int minimumMount;

public MinimumMount(int minimumMount) {
validate(minimumMount);
this.minimumMount = minimumMount;
}

private void validate(int mount) {
if (mount < MINIMUM_MOUNT) {
throw new IllegalArgumentException("최소 주문 금액은 " + MINIMUM_MOUNT + "원 이상이어야 합니다.");
}
if (mount > MAXIMUM_MOUNT) {
throw new IllegalArgumentException("최소 주문 금액은 " + MAXIMUM_MOUNT + "원 이하여야 합니다.");
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/coupon/domain/coupon/Period.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package coupon.domain.coupon;

import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class Period {

private LocalDate startDate;
private LocalDate endDate;

public Period(LocalDate startDate, LocalDate endDate) {
validate(startDate, endDate);
this.startDate = startDate;
this.endDate = endDate;
}

private void validate(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다.");
}
}
Comment on lines +20 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래의 요구사항은 어디서 반영이 되었을까요? 😄

  • 발급 기간
    • 시작일은 종료일보다 이전이어야 한다. 시작일과 종료일이 같다면, 해당 일에만 발급할 수 있는 쿠폰이 된다.
      • 시작일 00:00:00.000000 부터 발급할 수 있다.
      • 종료일 23:59:59.999999 까지 발급할 수 있다.

}
27 changes: 27 additions & 0 deletions src/main/java/coupon/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package coupon.domain.member;

import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private MemberName memberName;

public Member(Long id, MemberName memberName) {
this.id = id;
this.memberName = memberName;
}
}
25 changes: 25 additions & 0 deletions src/main/java/coupon/domain/member/MemberName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package coupon.domain.member;

import lombok.Getter;

@Getter
public class MemberName {

private static final int MAX_LENGTH = 30;

private final String name;

public MemberName(String name) {
validate(name);
this.name = name;
}

private void validate(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("이름은 비어있을 수 없습니다.");
}
if (name.length() > MAX_LENGTH) {
throw new IllegalArgumentException("이름은 최대 " + MAX_LENGTH + "자 입니다.");
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/coupon/repository/CouponRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package coupon.repository;

import coupon.domain.coupon.Coupon;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CouponRepository extends JpaRepository<Coupon, Long> {
}
Loading