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

version 2.6.0 #138

Merged
merged 4 commits into from
Feb 18, 2024
Merged
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
58 changes: 44 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,34 @@ Bounded Context(BC)는 동일한 문맥으로 효율적으로 업무 용어(도

---

## 2. 레거시 코드의 양방향 연관관계를 단방향으로!
## 2. 헥사고날 아키택처로의 전환

**문제 상황**

- 기존 아키택처의 한계점 ([데이터 중심의 설계가 불러온 한계](https://blogshine.tistory.com/688))
- 정확한 기준 없이 그때 그때 달라지는 페키지 구조와 네이밍 방식
- 확장에 대한 고려가 없는 기존의 설계

**문제 해결**

- 기존의 아키텍처를 헥사고날 아키텍처로 전환하면서 이를 극복

`유연성, 유지보수성` : 외부 시스템이나 인프라와의 의존성을 낮추어, 구성 요소를 쉽게 교체하거나 업데이트할 수 있게 되었습니다.
그도 그럴 것이 application의 service들은 모두 인터페이스에 해당되는 port에 의존하게 되었습니다.
더 이상 실 구현체가 아니기 때문에 중간에 다른 구현채로 변경되어도 유연하게 대응할 수 있는 장점을 갖게 되었죠! 이는 곧 유지보수성과도 직결된다 생각되더라고요!

`테스트 용이성` : 비즈니스 로직을 독립적으로 테스트할 수 있어 품질 향상과 개발 속도 향상에 도움이 됩니다!
인터페이스를 적절하게 사용하였기에 해당 로직의 독립성을 유지할 수 있던 점이 매우 장점이 돼준 것 같습니다.

`팀원과의 협업` : 책임이 분리되어 있어, 코드의 이해와 수정이 용이하며, 변화에 빠르게 대응할 수 있습니다.
즉, 흔하게 말하는 SOLID가 모두 지켜지고 있는 좋은 아키텍처 구조입니다.
또한 제2의 멤버가 들어와서 유지보수를 하거나 개편해야 해도 HexagonalArchitecture 자체의 이해도만 있다면 충분히 빠른 적응이 가능하다 생각됩니다.

**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/689))**

---

## 3. 레거시 코드의 양방향 연관관계를 단방향으로!
JPA에 대해서는 서로 어느 정도 이해하고 있어, 적절한 fetch join을 사용하여 코딩했었기에 N+1 문제는 발생하지 않았습니다.
하지만 연관관계에 대해서 문제가 있었습니다.
가장 좋은 연관관계 설계는 단방향을 기초로 하되 필요하면 양방향 설계를 하는 것입니다.
Expand Down Expand Up @@ -177,7 +204,7 @@ https://blogshine.tistory.com/345

---

## 3. 공지 Scrap작업 multi-threading 처리로 시간 개선하기
## 4. 공지 Scrap작업 multi-threading 처리로 시간 개선하기

**문제 상황**

Expand All @@ -192,7 +219,8 @@ https://blogshine.tistory.com/345
**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/660))**

---
## 4. Full-Text-Index도입을 통한 **검색 성능개선**

## 5. Full-Text-Index도입을 통한 **검색 성능개선**

**문제 상황**

Expand All @@ -208,7 +236,8 @@ https://blogshine.tistory.com/345
**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/664))**

---
## 5. HeapDump를 통해 메모리 누수 원인 찾기 **검색 성능개선**

## 6. HeapDump를 통해 메모리 누수 원인 찾기 **검색 성능개선**

**문제 상황**

Expand All @@ -224,7 +253,8 @@ https://blogshine.tistory.com/345
**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/687))**

---
# 6. Bulk Query를 통한 성능 개선

## 7. Bulk Query를 통한 성능 개선

**문제 상황**

Expand All @@ -243,7 +273,7 @@ https://blogshine.tistory.com/345

<br>

### 6-1) Insert 해결책
### 7-1) Insert 해결책

해결책은 2가지가 존재했습니다.
1. Table Id strategy를 SEQUENCE로 변경하고 Batch 작업
Expand All @@ -258,14 +288,14 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하

<br>

### 6-2) Delete 해결책
### 7-2) Delete 해결책
이미 프로젝트에서 queryDsl를 사용하고 있어 이를 이용하는 것이 가장 간단했기 때문에 queryDsl의 delete in 쿼리를 사용하여 해결했습니다.

**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/686))**

---

## 7. 인증, 인가를 비즈니스 로직으로부터 분리하기
## 8. 인증, 인가를 비즈니스 로직으로부터 분리하기

**문제 상황**

Expand All @@ -283,7 +313,7 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하

---

## 8. 흔하디 흔한 N+1 쿼리 개선기
## 9. 흔하디 흔한 N+1 쿼리 개선기

원래 로직에서는 사용자의 Category 이름 목록을 가져오기 위해서 다음과 같이 처리가 되고 잇었습니다!

Expand Down Expand Up @@ -341,7 +371,7 @@ public List<String> getCategoryNamesFromCategories(List<Category> categories) {

쿼리가 총 1 + 2N 만큼 발생중이다.

### 8 - 1) 변경 전 쿼리
### 9 - 1) 변경 전 쿼리

```bash
Hibernate:
Expand Down Expand Up @@ -398,7 +428,7 @@ Connection: keep-alive
N+1 문제로 User한번 조회하는데 위와 같이 쿼리가 3번 나가게 됨
### 8 - 2) 변경 후
### 9 - 2) 변경 후
변경 후 한방 쿼리로 조회 끝
```java
Expand All @@ -415,7 +445,7 @@ public List<String> getUserCategoryNamesByToken(String token) {
___
## 9. Test Container를 통한 테스트의 멱등성 보장하기
## 10. Test Container를 통한 테스트의 멱등성 보장하기
테스트와, 실제 운영 DB를 둘다 MariaDB 환경으로 사용하여 문제가 발생할 일이 없다 생각했었습니다.
하지만, utf8과 같은 인코딩 방식이 로컬과 프로덕션이 달라 문제가 발생하였으며, 이또한 테스트 환경에서 걸러내지 못한 것이 문제라 생각하였습니다.
Expand All @@ -424,7 +454,7 @@ ___
---
## 10. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화
## 11. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화
**문제 상황**
Expand All @@ -441,7 +471,7 @@ ___
---
## 11. 서버 모니터링
## 12. 서버 모니터링
**문제 상황**
Expand Down
79 changes: 18 additions & 61 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ buildscript {
}

plugins {
id 'org.springframework.boot' version '2.5.5'
id 'org.springframework.boot' version '2.7.18'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'com.ewerk.gradle.plugins.querydsl' version "1.0.10"
id 'java'
id 'org.asciidoctor.jvm.convert' version "3.3.2"
id 'org.flywaydb.flyway' version '9.16.1' // flyway gradle plugin 의존성
id 'org.sonarqube' version '3.5.0.2730' // sonarqube gradle plugin 의존성
id 'jacoco' // jacoco gradle plugin 의존성
}

group = 'com.kustacks'
version = '1.1.2'
version = '2.5.0'
sourceCompatibility = '17'

configurations {
Expand All @@ -41,12 +40,9 @@ sonarqube {
dependencies {
// Web
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'
implementation 'org.springframework.session:spring-session:1.3.5.RELEASE'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// DB
Expand All @@ -69,7 +65,7 @@ dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'

// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

// DevTool
compileOnly 'org.projectlombok:lombok'
Expand All @@ -85,30 +81,30 @@ dependencies {
// Firebase
implementation 'com.google.firebase:firebase-admin:8.1.0'

// API Docs
// RestDocs
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

// Swagger
implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:4.2.0'

// ArchUnit
testImplementation 'com.tngtech.archunit:archunit-junit5:1.0.1'

// Test Container
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
testImplementation 'org.testcontainers:testcontainers:1.17.6'
testImplementation 'org.testcontainers:junit-jupiter:1.17.6'
testImplementation 'org.testcontainers:mariadb:1.17.6'
}

ext {
snippetsDir = file 'build/generated-snippets'
testImplementation 'org.testcontainers:testcontainers:1.19.3'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
testImplementation 'org.testcontainers:mariadb:1.19.3'
}

test.onlyIf { System.getenv('DEPLOY_ENV') == 'dev' }

test {
outputs.dir snippetsDir

jacoco {
destinationFile = file("$buildDir/jacoco/jacoco.exec")
}
Expand All @@ -126,50 +122,6 @@ test {
finalizedBy 'jacocoTestReport'
}

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}

asciidoctor.onlyIf { System.getenv('DEPLOY_ENV') == 'dev' }

asciidoctor {
inputs.dir snippetsDir
dependsOn test
attributes "snippets": snippetsDir,
"version": version,
"stylesheet": "asciitheme/clean.css"

doFirst {
println "=====Start asciidoctor"
//asciidoctor 실행전 기존에 생성된 API 문서 삭제
delete file('src/main/resources/static/docs/api-docs.html')
}

doLast {
println "=====Finish asciidoctor"
}
}

task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/asciidoc/html5")
// resources/static/docs 로 복사하여 서버가 돌아가고 있을때 /docs/index.html 로 접속하면 볼수 있음
into file("src/main/resources/static/docs")
}

build {
dependsOn copyDocument
}

bootJar {
enabled = true
dependsOn asciidoctor
from("${asciidoctor.outputDir}/html5") {
into "static/docs"
}
}

jar {
enabled = false
manifest {
Expand All @@ -196,6 +148,11 @@ compileQuerydsl.doFirst {
if(file(querydslDir).exists())
delete(file(querydslDir))
}

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
delete file('src/main/generated')
}
// -- Jacoco 설정 -------------------------------------------------------
jacoco {
// jacoco version
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.kustacks.kuring.admin.adapter.in.web;

import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ADMIN_REAL_NOTICE_CREATE_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ADMIN_TEST_NOTICE_CREATE_SUCCESS;

import com.kustacks.kuring.admin.adapter.in.web.dto.RealNotificationRequest;
import com.kustacks.kuring.admin.adapter.in.web.dto.TestNotificationRequest;
import com.kustacks.kuring.admin.application.port.in.AdminCommandUseCase;
import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand;
import com.kustacks.kuring.admin.domain.AdminRole;
import com.kustacks.kuring.auth.authorization.AuthenticationPrincipal;
import com.kustacks.kuring.auth.context.Authentication;
import com.kustacks.kuring.auth.secured.Secured;
import com.kustacks.kuring.common.dto.BaseResponse;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Admin-Command", description = "관리자가 주체가 되는 정보 수정")
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api/v2/admin", produces = MediaType.APPLICATION_JSON_VALUE)
public class AdminCommandApiV2 {

private final AdminCommandUseCase adminCommandUseCase;

@Operation(summary = "테스트 공지 전송", description = "테스트 공지를 전송합니다, 실제 운영시 사용하지 않습니다")
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@PostMapping("/notices/dev")
public ResponseEntity<BaseResponse<String>> createTestNotice(
@RequestBody TestNotificationRequest request
) {
adminCommandUseCase.createTestNotice(request.toCommand());
return ResponseEntity.ok().body(new BaseResponse<>(ADMIN_TEST_NOTICE_CREATE_SUCCESS, null));
}

@Operation(summary = "전체 공지 전송", description = "전체 공지를 전송합니다, 실제 운영시 사용합니다")
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@PostMapping("/notices/prod")
public ResponseEntity<BaseResponse<String>> createRealNotice(
@RequestBody RealNotificationRequest request,
@AuthenticationPrincipal Authentication authentication
) {
RealNotificationCommand command = request.toCommandWithAuthentication(authentication);
adminCommandUseCase.createRealNoticeForAllUser(command);
return ResponseEntity.ok().body(new BaseResponse<>(ADMIN_REAL_NOTICE_CREATE_SUCCESS, null));
}

@Hidden
@Secured(AdminRole.ROLE_ROOT)
@GetMapping("/subscribe/all")
public ResponseEntity<Void> subscribe() {
adminCommandUseCase.subscribeAllUserSameTopic();
return ResponseEntity.ok().build();
}
}
Loading
Loading