Skip to content

Commit

Permalink
기본이 중요하다
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Diger committed Oct 26, 2023
1 parent 1601bd0 commit c2158cd
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 197 deletions.
21 changes: 21 additions & 0 deletions _posts/2023-01-01-SHORTS-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Disallow: /

Jsoup을 이용해서 컨텐츠를 크롤링하기로 했으니 DOM구조를 파악해야한다. 그 후 큰 틀의 DOM구조를 파싱할 수 있는 코드를 작성한다.

---

## setup() 메서드 호출 시

![image](https://github.com/mash-up-kr/SeeYouAgain_Spring/assets/60564431/36b5d26d-3e7f-4228-85e5-ec4d268a0bfa)
Expand Down Expand Up @@ -75,6 +77,7 @@ for (link in allHeadLineNewsLinks) {

여기서 담겨있는 N개의 기사에 대한 링크를 전부 받아온다.

---

```kotlin
val detailLink = Jsoup.parse(htmlLink)
Expand Down Expand Up @@ -380,6 +383,8 @@ class CrawlerCore(

크롤링하면서 OOM이 터졌다는 것 같은데 왜 터진지 알아보자

---

### 해결 과정 1. 서버 스펙 살펴보기

기존 서버 스펙은 NCPCompact옵션인 (CPU: 2CORE, Memory : 2GB)를 사용하고 있었다.
Expand All @@ -398,6 +403,8 @@ class CrawlerCore(

여튼 기본값을 지정된 초기 힙 크기를 가지고 크롤러를 돌릴 때 문제가 발생한다고 하니까 이를 해결해보자.

---

### 해결 과정 2. 서버의 수직적 확장

우선 JVM의 절대적인 Heap 공간이 모자라다는 것이 문제다. 로컬 환경에서의 모니터링을 통해 내린 결론은 크롤러가 요구하는 Memory Size는 약 2GB 언저리이다.
Expand All @@ -416,6 +423,8 @@ Exception in thread "Catalina-utility-2" java.lang.OutOfMemoryError: Java heap s

여전히 Heap 공간이 모자란 것인데, 이 역시 JVM의 기본 값으로 Heap 사이즈를 설정했기 때문에 그 사이즈로도 감당이 안된다는 것이다. 스펙업을 통해 얻은 여유 공간의 메모리를 조금 더 할당하도록 튜닝해야한다.

---

### 해결 과정 3. 튜닝 명령어 - Heap Size 확장, GC 지정

```shell
Expand All @@ -432,6 +441,8 @@ java -Xms256m -Xmx2048m -XX:+UseG1GC

시스템의 메모리와 Heap Size의 권장 비율을 알아보는 것도 좋을 것 같다.

---

## 문제점 2. DB 커넥션 부족 현상 발생

기존 로직은 크롤링을 통해 얻은 데이터의 갯수만큼 반복문을 순회하며 쿼리를 보내는 로직이였다.
Expand All @@ -446,6 +457,8 @@ java -Xms256m -Xmx2048m -XX:+UseG1GC

이를 한 커넥션에서 해결할 수 있는 방법이 벌크 삽입이라고 알게되어 실제 코드로 적용하는 방법을 알아보기 시작했다.

---

### Spring Data JPA - saveAll()

`saveAll()` 메서드의 구현 부분을 보면 트랜잭션은 하나로 가져가되 그 내부에서 반복문을 통해 다수의 데이터를 삽입하는 로직으로 구성되어있다.
Expand All @@ -460,6 +473,8 @@ java -Xms256m -Xmx2048m -XX:+UseG1GC

로직을 수행하는 시간 자체가 줄었으나 완전한 방법은 아니라고 생각했다.

---

### JdbcTemplate

`saveAll()` 메서드는 JPA Hibernate가 만들어주는 쿼리를 사용하기 때문에 결국에 부가적인 작업이 더 필요하다.
Expand Down Expand Up @@ -547,12 +562,16 @@ class NewsBulkInsertRepository(
}
```

---

### 이 데이터를 벌크 삽입으로 했을 때 문제점은 없는가?

- 트랜잭션 자체의 크기가 커지기 때문에 작업이 중간에 실패하면 데이터의 무결성이 깨질 수 있다.
- 롤백 시에도 많은 시간이 소요될 수 있다.
- 트랜잭션의 크기 자체가 크기 때문에 커넥션을 오래 가지고 있는다는 문제점이있다.

---

### 벌크 삽입의 문제점을 어떻게 해결할 수 있을까?

- 삽입할 데이터를 더 작은 단위들로 분할하여 삽입하도록 한다.
Expand All @@ -569,6 +588,8 @@ class NewsBulkInsertRepository(

그래서 이런 네트워크 문제로 예상되는 예외가 발생했을 때 재시도를하여 원하는 타이밍에 데이터를 삽입하진 못하더라도 결국에는 데이터를 유실하지 않도록 할 수 있는 방법을 알아보기로 했다.

---

### @Retryable

@Retryable 애노테이션을 활용하면 위와 같은 상황에서 재시도를 수행할 수 있게 된다.
Expand Down
8 changes: 8 additions & 0 deletions _posts/2023-01-01-SHORTS-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ mermaid: true
- [Lucene Korean Analyze](https://lucene.apache.org/core/7_4_0/analyzers-nori/org/apache/lucene/analysis/ko/KoreanAnalyzer.html)
- [Komoran](https://docs.komoran.kr/)

---

### 후보 1. Lucene Korean Analyzer

-[Lucene Nori Korean Analyze](https://m.blog.naver.com/websearch/221795964259)
Expand All @@ -31,6 +33,8 @@ mermaid: true

[Lucene Analyzer에 대한 조사](https://k-diger.github.io/posts/ApacheLucene/)

---

### 후보 2. Komoran

1. Jsoup을 활용하여 뉴스 크롤링
Expand Down Expand Up @@ -73,6 +77,8 @@ internal fun extractKeyword(content: String): String {
}
```

---

### Komoran

```kotlin
Expand All @@ -98,6 +104,8 @@ internal fun extractKeywordV2(content: String): String {
}
```

---

## 3. 채택 근거

본문의 내용 중 정확도가 더 높아 보이는 오픈소스를 활용하기로 결정하여 Komoran을 채택합니다.
Expand Down
172 changes: 172 additions & 0 deletions _posts/2023-01-01-SHORTS-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---

title: SHORTS - AOP를 활용하여 사용자 인증/인가 로직 간소화
date: 2023-01-01
categories: [SHORTS]
tags: [SHORTS]
layout: post
toc: true
math: true
mermaid: true

---

# AOP를 활용하여 사용자 인증/인가 로직 간소화

## 왜 이런 과정이 필요했는지?

기존 코드에는 특정 API 컨트롤러마다 사용자 인증 정보를 가져오는 로직이 반복되고있었다.

컨트롤러에서 이에 대한 관심사를 해결하는 것 보다는 이를 분리하는게 더 역할에 맞다고 생각해서 이를 분리하기로 했다.

---

## 구체적으로 어떻게 구현한건지?

HTTP Connection을 맺고 있는 Thread의 ThreadLocal에 서버에서 직접 발급하고 데이터베이스에서 관리하는 사용자의 UUID를 등록된 인증 정보를 보관하도록 하고

이 인증 정보를 사용할 수 있는 로직을 전역적으로 선언하여 Spring 내부의 계층에서 자유롭게 사용할 수 있는 로직을 작성했다.

그리고 이 로직을 사용할 수 있는 대상을 애노테이션으로 지정할 수 있게 하여 반복되어 등장하는 사용자 인증 정보를 꺼내는 로직을 제거했다.

### Auth.kt

```kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Auth
```

### AuthAspect.kt

```kotlin
@Aspect
@Component
class AuthAspect(
private val httpServletRequest: HttpServletRequest,
private val memberRepository: MemberRepository
) {

@Around("@annotation($SHORTS_PACKAGE)")
fun memberId(pjp: ProceedingJoinPoint): Any {
val memberId = resolveToken(httpServletRequest)
?: throw ShortsBaseException.from(
shortsErrorCode = ShortsErrorCode.E401_UNAUTHORIZED,
"Request Header에 memberId가 존재하지 않습니다."
)

val member = memberRepository.findByUniqueId(memberId)
?: throw ShortsBaseException.from(
shortsErrorCode = ShortsErrorCode.E404_NOT_FOUND,
resultErrorMessage = "존재하지 않는 유저입니다. memberId : $memberId"
)

AuthContext.USER_CONTEXT.set(member)
return pjp.proceed(pjp.args)
}

private fun resolveToken(request: HttpServletRequest): String? {
val bearerToken = request.getHeader(AUTHORIZATION)
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(PREFIX_BEARER)) {
return bearerToken.substring(7)
}

return null
}

companion object {
private const val AUTHORIZATION = "Authorization"
private const val PREFIX_BEARER = "Bearer "
private const val SHORTS_PACKAGE = "com.mashup.shorts.common.aop.Auth"
}
}
```

### AuthContext.kt

```kotlin
object AuthContext {

val USER_CONTEXT: ThreadLocal<Member> = ThreadLocal()

fun getMember(): Member {
USER_CONTEXT.get()?.let {
return USER_CONTEXT.get()
} ?: throw ShortsBaseException.from(
shortsErrorCode = ShortsErrorCode.E401_UNAUTHORIZED,
resultErrorMessage = "인증 체크 중에 ThreadLocal 값을 꺼내오는 중에 문제가 발생했습니다."
)
}
}
```

---

## 사용자는 그러면 어떻게 자신의 UUID를 가지고 있는가?

클라이언트의 로컬 스토리지나 내부 DB에 저장하여 매 요청마다 인증 헤더에 실어 보내야한다.

---

## 해당 UUID가 탈취되었을 때의 문제점과 해결방안은?

DB에서 탈취된 것으로 판단된 UUID를 제거하여 피해를 막는 사후조치를 해야할 것 같다.

---

## 이 과정 자체가 그러면 토큰 기반 방식의 인증 방법일까? 세션 기반 방식의 인증 방법일까?

세션 기반 인증 방식으로 볼 수 있을 것 같다.

---

### 위 질의 응답에 관한 근거

사용자의 UUID를 서버에서 직접 발급하고 서버 내부에서 관리하는 것이므로 세션 기반 인증 방식에 가깝다고 할 수 있을 것 같다.

하지만 서버 내부의 메모리에서 해당 인증 정보를 관리하는 것이 아닌 DB에 저장되어있는 내용을 관리하는 것이기 때문에 완전한 세션방식이라고 하기엔 조금 어려울 수도 있을 것 같다.

세션 기반 인증에서는 서버 측에서 세션을 관리하고, 클라이언트에게 세션 ID를 부여하여 이를 사용자 식별에 활용한다.

---

## 공격자로부터 클라이언트의 인증 정보가 탈취되었음을 서버측에서는 어떻게 알 수 있을까?

- 클라이언트의 UUID가 이전에 없던 위치에서 사용되었거나, 단기간 내에 많은 요청이 발생하는 경우 이상행동으로 간주하여 해당 세션을 무효화한다.

- 클라이언트의 로그인 위치를 기록하고, 동일한 UUID가 다른 지역에서 사용되는 경우 해당 세션을 무효화한다.

---

## 토큰 기반 인증 방식 장/단점

- 장점 1. 확장성과 분산화
- JWT는 토큰을 생성하고 검증하는 키를 기반으로 동작하며, 토큰에 필요한 정보를 담을 수 있어서 서버 간에 토큰을 공유하거나 전달할 수 있어 확장성이 뛰어나고 분산 환경에서 사용하기 용이하다.

- 장점 2. 상태 없음(Stateless)
- 서버 측에서 토큰을 검증하고 필요한 정보를 추출하므로, 서버는 클라이언트의 상태를 저장할 필요가 없어 리소스가 절약 될 수 있다.

- 장점 3. 유연한 사용자 권한 관리
- 토큰 내에 사용자 권한과 관련된 정보를 포함하여 사용자 권한 관리가 용이하며, 토큰의 내용을 이용하여 권한 검사를 수행할 수 있다.

- 단점 1. 토큰 크기와 보안
- JWT는 탈취될 가능성이 있다. 중요한 정보를 토큰에 포함시키면 보안 문제가 발생할 수 있다.

- 단점 2. 토큰 유효성 검증의 어려움
- 토큰이 변조되지 않았는지 확인하기 위해 서명을 검증해야 하기 때문에 서명 검증 과정이 추가로 필요하며, 이에 따른 복잡성이 발생할 수 있다.

---

## 서버 측 세션(Session) 기반 인증 방식 장/단점

- 장점 1. 보안성
- 세션은 서버에 저장되므로 클라이언트에 노출되지 않는다. 토큰 기반 인증에 비해 보안성이 높다.

- 장점 2. 세션 탈취 시 대처가능
- 세션을 사용하면 만료 시간을 쉽게 조절하고 조절할 수 있으며, 만료 시간이 지나면 자동으로 세션을 무효화시킬 수 있다.

- 단점 1. 상태 유지
- 세션은 서버 측에서 상태를 유지해야 하므로, 서버의 메모리를 사용하게 되어 클라이언트가 많을 때 성능 저하가 발생할 수 있다.

- 단점 2. 확장성
- 분산 환경에서 각 서버마다 발급하는 세션을 관리하기 위해 세션 클러스터를 운영해야하는 복잡성이 증가한다.
Loading

0 comments on commit c2158cd

Please sign in to comment.