Skip to content

sudosoo/TakeItEasy

Repository files navigation

Toy Project

Post(Kotlin,JAVA) : https://github.com/sudosoo/TakeItEasy/

Event(Kotlin) : https://github.com/sudosoo/TakeItEasyEvent/

MemberInfo(Kotlin) : https://github.com/sudosoo/TakeItEasyAdmin/

Dependencies

Environment

  • Java Version: 17
  • Build Tool: Gradle
  • Dependency Management: Spring.dependency-management

Event Sourcing

  • Kafka

Log & Search

  • ELK Stack + filebeat

Cache

  • Redis

DataBase

  • H2
  • PostgreSQL

Libraries

  • Spring Boot 3.x
  • Spring Boot Starter Data JPA
  • Spring Boot Starter Web
  • Spring Boot Starter Validation
  • Spring Boot DevTools
  • Spring Boot Starter Test
  • Spring Boot starter batch
  • Spring RestDocs MockMvc
  • Mockk
  • JUnit5

Infra structure



AOP 로깅 / 중복 요청 방지 / 실시간 알림 ( SSE + kafka)

[ AOP Flow ]

[🚗 중복요청방지 Blog Visit (https://soobysu.tistory.com/125)

[🐰 실시간 알림 Blog Visit (https://soobysu.tistory.com/130)

[🐻 AOP로깅 Code

@Around("onRequest()")
public Object requestLogging(ProceedingJoinPoint joinPoint) throws Throwable {
// API 요청 정보
final RequestApiInfo apiInfo = new RequestApiInfo(joinPoint, joinPoint.getTarget().getClass(), objectMapper);
// 로그 정보
final LogInfo logInfo = new LogInfo(
apiInfo.getUrl(),
apiInfo.getName(),
apiInfo.getMethod(),
apiInfo.getHeader(),
objectMapper.writeValueAsString(apiInfo.getParameters()),
objectMapper.writeValueAsString(apiInfo.getBody()),
apiInfo.getIpAddress()
);
try {
final Object result = joinPoint.proceed(joinPoint.getArgs());
// Method가 Get이 아닌 로그만 수집
if (!logInfo.getMethod().equals("GET")) {
final String logMessage = objectMapper.writeValueAsString(Map.entry("logInfo", logInfo));
logger.info(logMessage);
}
return result;
} catch (Exception e) {
final StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
final String exceptionAsString = sw.toString();
// 발생 Exception 설정
logInfo.setException(exceptionAsString);
final String logMessage = objectMapper.writeValueAsString(logInfo);
logger.error(logMessage);
throw e;
}
}
}


MSA 서버간 Kafka Evnet 비동기 통신

[📡 MSA 서버간 Kafka Evnet 비동기 통신 Blog Visit(https://soobysu.tistory.com/135)]

public Object replyRecord(Object requestData) throws ExecutionException, InterruptedException, JsonProcessingException {
String jsonData = objectMapper.writeValueAsString(requestData);
ProducerRecord<String, String> record = new ProducerRecord<String, String>(kafkaRestApiRequestTopic,jsonData);
record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, kafkaRestApiReplyTopic.getBytes()));
RequestReplyFuture<String, String, String> sendAndReceive = replyingKafkaTemplate.sendAndReceive(record);
ConsumerRecord<String, String> consumerRecord = sendAndReceive.get();
return consumerRecord.value();
}


모듈 분리 JAVA -> JAVA + Kotlin (책임 분리)

[🦔 계층 별 모듈 분리 (domain JAVA , 그 외 Kotlin) ]


ELK + filebeat 를 활용한 로그적재

[🐨 ELK Stack 로그적재 Blog Visit (https://soobysu.tistory.com/category/%EA%B0%9C-%EB%B0%9C/Infra?page=3)


젠킨스 + NginX + Docker CICD 무중단 배포

🐻‍❄️ 젠킨스 + NginX + Docker CICD 무중단 배포 Blog Visit


Redis 분산락 활용 동시성 제어 ( AOP 적용 )

[🐮 Redis 분산락 활용 Blog Visit (https://soobysu.tistory.com/136)

🚗 Visit TakeItEasyEvent Repo


TDD 기반 테스트코드 작성

[🐯 Test Code

@Test
@DisplayName("쿠폰 발급 테스트 (멀티 스레드)")
void couponIssuanceForMultiThreadTest() throws InterruptedException {
CouponIssuanceRequestDto couponIssuanceRequestDto = new CouponIssuanceRequestDto(1L,1L,1L);
AtomicInteger successCount = new AtomicInteger();
int numberOfExecute = 100;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfExecute);
Coupon mockCoupon = mock(Coupon.class);
Event event = Event.of(requestDto, LocalDateTime.now(), mockCoupon);
when(eventRepository.findByEventIdForUpdate(anyLong())).thenReturn(Optional.ofNullable(event));
for (int i = 0; i < numberOfExecute; i++) {
final int threadNumber = i + 1;
service.execute(() -> {
try {
eventService.couponIssuance(couponIssuanceRequestDto);
successCount.getAndIncrement();
System.out.println("Thread " + threadNumber + " - 성공");
} catch (PessimisticLockingFailureException e) {
System.out.println("Thread " + threadNumber + " - 락 충돌 감지");
} catch (Exception e) {
System.out.println("Thread " + threadNumber + " - " + e.getMessage());
}
latch.countDown();
});
}
latch.await();
// 성공한 경우의 수가 10개라고 가정.
assertThat(successCount.get()).isEqualTo(10);
}


CQRS 패턴 적용 Read 기능 분리

[🐮 Redis를 활용한 CQRS패턴 정용기 Blog Visit (https://soobysu.tistory.com/138)

public class RedisServiceImpl implements RedisService {
private final PostRepository postRepository;
private final RedisTemplate<String, String> redisTemplate;
public static ObjectMapper objectMapper = new ObjectMapper();
//pageable 객체로 key 값에 해당 하는 밸류를 page갯수만큼 불러온다.
@Override
public <T> List<T> findByPaginationPost(String responseDtoName, PageRequest pageable) {
Class<?> clazz = null;
try {
clazz = Class.forName(responseDtoName);
}catch (ClassNotFoundException e){
e.getStackTrace();
}
int pageNumber = pageable.getPageNumber(); // 현재 페이지 번호
int pageSize = pageable.getPageSize(); // 페이지 크기
int start = pageNumber * pageSize; // 시작 인덱스
int end = start + pageSize - 1; // 끝 인덱스
List<String> jsonValues = redisTemplate.opsForList().range(responseDtoName, start, end);
List<T> result = new ArrayList<>();
for (String responseDto : jsonValues) {
try {
T value = (T) objectMapper.readValue(responseDto, clazz);
result.add(value);
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
//메소드 이름을 key값으로 해당 하는 밸류들 불러온다
@Override
public <T> List<T> getValues(String className) {
List<T> result = new ArrayList<>();
try {
// 패키지명과 클래스명을 조합하여 전체 클래스 이름을 만듭니다.
String fullClassName = "com.sudosoo.takeiteasy.dto." + className;
Class<?> clazz = Class.forName(fullClassName);
List<String> jsonValues = redisTemplate.opsForList().range(className, 0, -1);
for (String responseDto : Objects.requireNonNull(jsonValues)) {
// Reflection을 사용하여 클래스의 객체 생성
T value = objectMapper.readValue(responseDto, (Class<T>) clazz);
result.add(value);
}
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return result;
}
@Override
public void saveReadValue(Object value) {
try{
String className = value.getClass().getSimpleName();
String jsonObject = objectMapper.writeValueAsString(value);
redisTemplate.opsForList().leftPush(className,jsonObject);
}catch (JsonProcessingException e ){
e.getStackTrace();
}
}
@Override
public void postRepositoryRedisSynchronization() {
redisTemplate.delete("PostResponseDto");
List<Post> posts = postRepository.findAll();
for (Post post : posts) {
try{
PostResponseDto postInstance = post.toResponseDto();
String jsonPost = objectMapper.writeValueAsString(postInstance);
redisTemplate.opsForList().leftPush("PostResponseDto",jsonPost);
}catch (JsonProcessingException e ){
e.getStackTrace();
}
}
}
}


Spring Batch ( Bulk insert )

10만건 db insert [ 28분 -> 28초 성능개선 ]

[🐥 Spring Batch ( Bulk insert ) Blog Visit (https://soobysu.tistory.com/131)

public void runBatchJob() {
String sql = "INSERT INTO post (title, content, category_id, member_id, view_count) VALUES (?, ?, ?, ?, ?)";
try {
con = dataSource.getConnection();
con.setAutoCommit(false);
pstmt = con.prepareStatement(sql);
for (int i = 0; i < 100000; i++) {
Post post = postService.createBatchPosts(i);
pstmt.setString(1, post.getTitle());
pstmt.setString(2, post.getContent());
pstmt.setLong(3, 2L);
pstmt.setLong(4, 2L);
pstmt.setInt(5, post.getViewCount());
// addBatch에 담기
pstmt.addBatch();
// 파라미터 Clear
pstmt.clearParameters();
if ((i % 10000) == 0) {
// Batch 실행
pstmt.executeBatch();
// Batch 초기화
pstmt.clearBatch();
// 커밋
con.commit();
}
}
pstmt.executeBatch();
con.commit();
} catch (Exception e) {
try {
con.rollback();
} catch (SQLException ignored) {;
}
} finally {
if (pstmt != null) try {
pstmt.close();
pstmt = null;
} catch (SQLException ignored) {
}
if (con != null) try {
con.close();
con = null;
} catch (SQLException ignored) {
}
}
}


테이블 index 전략 ( 검색 최적화 )

[🐷 테이블 index 전략 (검색 최적화) Blog Visit (https://soobysu.tistory.com/115)


Jasypt 중요 정보 암호화

[🐵 Jasypt 중요 정보 암호화 Blog Visit (https://soobysu.tistory.com/149)

#PostgreSQL
spring.datasource.url=ENC(i37HrXNxx1Gui4eR0WtuKuzUKXdjTtP1HbrdjPY1ZiW5sWeoQ5dDIqwjD8cw7NCOU85R30nirNRTLJ8hCnB1+w==)
spring.datasource.username=ENC(NFDmaWJq/gE2YoG9d9IfOQ==)
spring.datasource.password=ENC(iJXJwU6A8bQQJY8YQlEJ2V22UQax3r3D)


TODO

  • Bulk Update 대량의 데이터 중 몇건의 데이터 수정하기 (완료)
Project Structure tree
📦 
├─ .ignore
├─ HELP.md
├─ README.md
├─ build.gradle
├─ gradle
│  └─ wrapper
│     ├─ gradle-wrapper.jar
│     └─ gradle-wrapper.properties
├─ gradlew
├─ gradlew.bat
├─ images
│  ├─ AOPFlow.png
│  ├─ InfraStructure.png
│  ├─ InfraStructureDetail.png
│  ├─ KibanaLog.png
│  └─ jenkinsStatus.png
├─ settings.gradle
└─ src
   ├─ main
   │  ├─ java
   │  │  └─ com
   │  │     └─ sudosoo
   │  │        └─ takeiteasy
   │  │           ├─ TakeItEasyApplication.java
   │  │           ├─ aspect
   │  │           │  ├─ DuplicateRequestAspect.java
   │  │           │  ├─ LoggingAspect.java
   │  │           │  ├─ NotifyAspect.java
   │  │           │  ├─ logging
   │  │           │  │  ├─ LogInfo.java
   │  │           │  │  └─ RequestApiInfo.java
   │  │           │  └─ notice
   │  │           │     └─ NotifyInfo.java
   │  │           ├─ common
   │  │           │  ├─ BaseEntity.java
   │  │           │  ├─ CustomDateTimeFormat.java
   │  │           │  ├─ CustomNotify.java
   │  │           │  ├─ DateTimeFormatValidator.java
   │  │           │  └─ NotLogging.java
   │  │           ├─ config
   │  │           │  ├─ AbstractElasticsearchConfiguration.java
   │  │           │  ├─ ElkConfig.java
   │  │           │  └─ KafkaConfig.java
   │  │           ├─ controller
   │  │           │  ├─ CategoryController.java
   │  │           │  ├─ CommnetController.java
   │  │           │  ├─ CouponController.java
   │  │           │  ├─ EventController.java
   │  │           │  ├─ HeartController.java
   │  │           │  ├─ MemberController.java
   │  │           │  ├─ MessageController.java
   │  │           │  ├─ NoticeController.java
   │  │           │  ├─ PostController.java
   │  │           │  ├─ ProfileController.java
   │  │           │  └─ TestController.java
   │  │           ├─ dto
   │  │           │  ├─ category
   │  │           │  │  ├─ CategoryResponseDto.java
   │  │           │  │  └─ CreateCategoryRequestDto.java
   │  │           │  ├─ comment
   │  │           │  │  ├─ CommentResposeDto.java
   │  │           │  │  ├─ CreateCommentRequestDto.java
   │  │           │  │  └─ UpdateCommentRequestDto.java
   │  │           │  ├─ coupon
   │  │           │  │  └─ CouponIssuanceRequestDto.java
   │  │           │  ├─ event
   │  │           │  │  ├─ CreateEventRequestDto.java
   │  │           │  │  └─ EventResponseDto.java
   │  │           │  ├─ heart
   │  │           │  │  ├─ CommentHeartRequestDto.java
   │  │           │  │  └─ PostHeartRequestDto.java
   │  │           │  ├─ member
   │  │           │  │  └─ CreateMemberRequestDto.java
   │  │           │  ├─ message
   │  │           │  │  ├─ MentionRequestDto.java
   │  │           │  │  └─ MessageSendRequestDto.java
   │  │           │  ├─ notice
   │  │           │  │  ├─ NoticeRequestDto.java
   │  │           │  │  └─ NoticeResponseDto.java
   │  │           │  └─ post
   │  │           │     ├─ CreatePostRequestDto.java
   │  │           │     ├─ PostDetailResponsetDto.java
   │  │           │     ├─ PostTitleDto.java
   │  │           │     ├─ SetCategoryByPostRequestDto.java
   │  │           │     └─ UpdatePostRequestDto.java
   │  │           ├─ entity
   │  │           │  ├─ Category.java
   │  │           │  ├─ Comment.java
   │  │           │  ├─ Coupon.java
   │  │           │  ├─ Event.java
   │  │           │  ├─ Heart.java
   │  │           │  ├─ HeartType.java
   │  │           │  ├─ Member.java
   │  │           │  ├─ Message.java
   │  │           │  ├─ MessageType.java
   │  │           │  ├─ Notice.java
   │  │           │  ├─ NoticeType.java
   │  │           │  └─ Post.java
   │  │           ├─ kafka
   │  │           │  ├─ KafkaConsumer.java
   │  │           │  └─ KafkaProducer.java
   │  │           ├─ repository
   │  │           │  ├─ CategoryRepository.java
   │  │           │  ├─ CommentRepository.java
   │  │           │  ├─ CouponRepository.java
   │  │           │  ├─ EmitterRepository.java
   │  │           │  ├─ EmitterRepositoryImpl.java
   │  │           │  ├─ EventRepository.java
   │  │           │  ├─ HeartRepository.java
   │  │           │  ├─ MemberRepository.java
   │  │           │  ├─ MessageRepository.java
   │  │           │  ├─ NoticeRepository.java
   │  │           │  └─ PostRepository.java
   │  │           └─ service
   │  │              ├─ CategoryService.java
   │  │              ├─ CategoryServiceImpl.java
   │  │              ├─ CommentService.java
   │  │              ├─ CommentServiceImpl.java
   │  │              ├─ CouponService.java
   │  │              ├─ CouponServiceImpl.java
   │  │              ├─ EventService.java
   │  │              ├─ EventServiceImpl.java
   │  │              ├─ HeartService.java
   │  │              ├─ HeartServiceImpl.java
   │  │              ├─ MemberService.java
   │  │              ├─ MemberServiceImpl.java
   │  │              ├─ MessageService.java
   │  │              ├─ MessageServiceImpl.java
   │  │              ├─ NoticeService.java
   │  │              ├─ NoticeServiceImpl.java
   │  │              ├─ PostService.java
   │  │              └─ PostServiceImpl.java
   │  └─ resources
   │     ├─ .gitkeep
   │     └─ config
   │        └─ .gitkeep
   └─ test
      └─ java
         └─ com
            └─ sudosoo
               └─ takeiteasy
                  ├─ TakeItEasyApplicationTests.java
                  └─ service
                     ├─ CategoryServiceImplTest.java
                     ├─ CommentServiceImplTest.java
                     ├─ CouponServiceImplTest.java
                     ├─ EventServiceImplTest.java
                     ├─ HeartServiceImplTest.java
                     ├─ MemberServiceImplTest.java
                     └─ PostServiceImplTest.java