개발을 하다 보면 JPA와 Hibernate를 사용하는 환경에서 LazyInitializationException을 마주치게 될 때가 있습니다. 특히 연관 데이터를 지연 로딩(Lazy Loading)으로 설정했을 때 트랜잭션 종료 이후 데이터를 로드하려 하면 이 문제가 발생합니다. 이번 글에서는 LazyInitializationException이 무엇인지, 왜 발생하는지, 그리고 이를 해결하기 위해 필요한 트랜잭션, 영속성 컨텍스트, 준영속성에 대해 설명합니다.

 

 

 

1. LazyInitializationException이란?

LazyInitializationException은 JPA에서 지연 로딩(Lazy Loading)으로 설정된 연관 데이터를 트랜잭션 종료 이후 로드하려고 할 때 발생하는 예외입니다. Lazy Loading은 실제로 데이터가 필요할 때만 쿼리를 실행하여 데이터를 가져오는 방식이지만, 트랜잭션이 종료된 후에는 영속성 컨텍스트가 닫혀 데이터베이스에 접근할 수 없으므로 예외가 발생합니다.

 

 

2. 예제 코드

Circle 엔티티와 CircleSchedule 엔티티를 사용한 예제를 통해 설명합니다.

@Entity
class Circle(
    @Id
    @GeneratedValue
    val id: UUID = UUID.randomUUID(),

    val title: String,

    @OneToMany(mappedBy = "circle", fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
    val circleSchedules: MutableList<CircleSchedule> = mutableListOf()
)


CircleSchedule 엔티티

@Entity
class CircleSchedule(
    @Id
    @GeneratedValue
    val id: UUID = UUID.randomUUID(),

    @ManyToOne(fetch = FetchType.LAZY)
    val circle: Circle,

    val dayOfWeek: String,
    val startTime: String,
    val endTime: String
)

 

 

3. 문제 상황: LazyInitializationException 발생

아래 코드에서 circle.circleSchedules는 Lazy Loading으로 설정되어 있습니다. 트랜잭션이 종료된 후 circleSchedules에 접근하려 하면 LazyInitializationException이 발생합니다.

 

문제 코드

fun getCircleSchedules(circleId: UUID): List<CircleSchedule> {
    val circle = circleRepository.findById(circleId)
        .orElseThrow { NotExistException("Circle not found") }
    return circle.circleSchedules // LazyInitializationException 발생 가능
}

 

발생 이유

  1. circle.circleSchedules는 FetchType.LAZY로 설정되어 있어, 기본적으로 데이터베이스 쿼리를 실행하지 않습니다.
  2. 트랜잭션이 종료되면 영속성 컨텍스트도 종료되므로, 지연 로딩된 데이터를 가져올 수 없습니다.

 

4. LazyInitializationException 해결 방법

1) @Transactional 사용

트랜잭션 내에서 지연 로딩 데이터에 접근하도록 @Transactional을 적용합니다.

@Transactional(readOnly = true)
fun getCircleSchedules(circleId: UUID): List<CircleSchedule> {
    val circle = circleRepository.findById(circleId)
        .orElseThrow { NotExistException("Circle not found") }
    return circle.circleSchedules // Lazy Loading 가능
}

 

2) Open-Session-In-View 설정

Spring Boot에서 spring.jpa.open-in-view 설정을 활성화하면 트랜잭션이 종료된 이후에도 Lazy Loading이 가능합니다. 하지만 이 방법은 권장되지 않습니다.

 

application.yml 설정:

spring:
  jpa:
    open-in-view: true

 

 

5. 영속성 컨텍스트와 트랜잭션

영속성 컨텍스트(Persistence Context)

영속성 컨텍스트는 JPA 엔티티를 관리하며 데이터베이스와의 상호작용을 처리하는 캐시 역할을 합니다.

  • 트랜잭션이 시작되면 영속성 컨텍스트가 활성화됩니다.
  • 트랜잭션이 종료되면 영속성 컨텍스트도 종료됩니다.
  • Lazy Loading은 영속성 컨텍스트 내에서만 작동합니다.

트랜잭션 종료 이후의 Lazy Loading

트랜잭션이 종료되면 영속성 컨텍스트가 종료되고, Lazy Loading 데이터는 로드할 수 없습니다. 이를 방지하려면 트랜잭션 내부에서 데이터를 로드해야 합니다.

 

 

6. 결론

  • LazyInitializationException은 트랜잭션 종료 이후 Lazy Loading 데이터를 로드하려 할 때 발생합니다.
  • @Transactional 등을 활용하여 LazyInitializationException을 방지할 수 있습니다.
  • 영속성 컨텍스트와 트랜잭션의 생명주기를 이해하면 Lazy Loading 문제를 더 잘 해결할 수 있습니다.

'JPA' 카테고리의 다른 글

Kotlin, JPA 환경에서 Entity 설계에 대한 고민  (0) 2024.09.08
자바 ORM 표준 JPA 프로그래밍 기본편 정리 1탄  (0) 2022.08.07
@Enumerated  (0) 2022.07.25

프로젝트를 진행하다 보면 다양한 조건을 기반으로 데이터를 검색해야 할 때가 많습니다. 예를 들어, title, englishLevel, city와 같은 조건을 이용해 Circle 데이터를 검색할 때, 이들 조건이 null일 경우 해당 조건을 무시하고 싶을 수 있습니다. 이런 경우 Kotlin JDSL에서는 null 값 조건을 쉽게 무시하는 방법을 제공하며, 이번 글에서는 이를 어떻게 적용할 수 있는지 설명드리겠습니다.

 

문제 상황

아래와 같은 쿼리를 작성했다고 가정해보겠습니다.

override fun findCirclesByPagination(pageable: Pageable, request: CircleSearchRequest?)
: Page<CirclePageResponse?> {
    return kotlinJdslJpqlExecutor.findPage(pageable) {
        selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike),
        ).from(
                entity(Circle::class),
                join(Circle::leader)
        ).whereAnd(
                path(Circle::title).like("%${request?.title}%"),
                path(Circle::englishLevel).eq(request?.level),
                path(Circle::city).eq(request?.city)
        )
    }
}

여기서 우리는 title, englishLevel, city 세 가지 조건을 사용하고 있습니다. 하지만 이 필드들 중 하나라도 null일 경우 쿼리에서 해당 조건을 무시해야 할 때 어떻게 해야 할까요?

 

해결 방법: 조건을 let으로 감싸기

Kotlin JDSL에서는 whereAnd 블록 내에서 조건을 동적으로 추가할 수 있습니다. let 함수를 활용해 각 조건이 null이 아닐 때만 적용되도록 설정해보겠습니다.

 

override fun findCirclesByPagination(pageable: Pageable, request: CircleSearchRequest?)
: Page<CirclePageResponse?> {
    return kotlinJdslJpqlExecutor.findPage(pageable) {
        selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike),
        ).from(
                entity(Circle::class),
                join(Circle::leader)
        ).whereAnd(
                request?.title?.let { path(Circle::title).like("%$it%") },
                request?.level?.let { path(Circle::englishLevel).eq(it) },
                request?.city?.let { path(Circle::city).eq(it) }
        )
    }
}

 

코드 설명

  1. 조건 추가를 위한 let 사용: request?.title, request?.level, request?.city가 각각 null이 아닐 때만 let 블록 내부의 조건이 실행되어 쿼리에 추가됩니다.
  2. null 값 무시: title, level, city 중 하나라도 null이면 해당 조건은 자동으로 쿼리에서 제외됩니다.

동작 방식 예시

  • title만 null인 경우: englishLevel과 city만 조건으로 포함됩니다.
  • 모든 값이 null인 경우: 조건 없이 전체 결과가 반환됩니다.

이를 통해 null 조건을 유연하게 무시하고 원하는 조건에 따라 필터링된 결과를 반환받을 수 있습니다.

 

마무리

Kotlin JDSL에서 조건을 동적으로 설정하는 것은 상당히 직관적이며, let 함수와 같은 Kotlin의 기능을 활용해 쿼리의 가독성을 높일 수 있습니다. 이 방식은 특히 검색 기능을 구현할 때 유용하며, nullable 필드를 조건으로 사용할 때 코드의 안정성과 효율성을 높여줍니다.

이번 글에서는 Kotlin JDSL을 사용해 조건이 null인 경우 쿼리에서 해당 조건을 제외하는 방법을 살펴보았습니다. 앞으로 JDSL을 활용해 다양한 쿼리를 동적으로 구성할 때 큰 도움이 되길 바랍니다.

'Kotlin-Jdsl' 카테고리의 다른 글

Kotlin JDSL 도입기: Page 타입 응답값 반환하기  (1) 2024.11.03

최근 프로젝트에서 Kotlin JDSL을 도입하며, 특히 페이지네이션된 데이터를 반환할 때 몇 가지 문제에 직면했습니다. 자료가 부족하거나 오래되어 직접 구현하기 쉽지 않았지만, 공식 문서와 GitHub 테스트 코드를 참고하여 문제를 해결할 수 있었습니다. 이번 글에서는 해결 과정을 코드와 함께 공유하고자 합니다.

문제 해결 과정: Page 타입 응답 구현

페이지네이션된 데이터를 반환하기 위해 KotlinJdslJpqlExecutor의 findPage 메서드와 selectNew를 사용했습니다. selectNew 메서드는 엔티티가 아닌 사용자 정의 DTO로 데이터를 매핑할 수 있어 매우 유용합니다. 아래는 구현한 코드입니다

@Repository
class CustomCircleRepositoryImpl(
    private val kotlinJdslJpqlExecutor: KotlinJdslJpqlExecutor
) : CustomCircleRepository {

    override fun findCirclesByPagination(pageable: Pageable): Page<CirclePageResponse?> {
        return kotlinJdslJpqlExecutor.findPage(pageable) {
            selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike)
            ).from(
                entity(Circle::class),
                join(Circle::leader)
            )
        }
    }
}

코드 설명

  1. CustomCircleRepositoryImpl 클래스: KotlinJdslJpqlExecutor를 주입받아 findCirclesByPagination 메서드를 구현한 커스텀 리포지토리입니다.
  2. findCirclesByPagination 메서드: Pageable 객체를 인자로 받아 Page<CirclePageResponse?> 타입의 응답을 반환합니다. 이 메서드는 JPA의 Page 기능을 활용해 페이지네이션된 데이터를 반환하도록 설계되었습니다.
  3. selectNew 메서드: Kotlin JDSL의 핵심 기능 중 하나로, CirclePageResponse라는 사용자 정의 DTO에 엔티티 데이터를 매핑합니다. path 메서드를 통해 각 필드를 선택하며, DTO 필드와 일치하도록 매핑합니다. Member 엔티티에서 리더의 프로필과 닉네임을 가져오기 위해 join을 사용했습니다.

DTO 설계와 발생한 이슈

페이지네이션을 위해 사용하는 CirclePageResponse는 다음과 같은 구조를 가지고 있습니다:

@Schema(description = "서클 페이지 조회 응답")
data class CirclePageResponse(
    val id: UUID,
    val thumbnailUrl: String? = null,
    val title: String,
    val introduction: String = "",
    val leaderProfile: String? = null,
    val leaderName: String,
    val englishLevel: EnglishLevel,
    val city: City,
    val capacity: Int,
    val totalView: Int,
    val totalLike: Int,
    val likedByMe: Boolean = false
)

이슈: selectNew 메서드의 제한 사항

selectNew를 사용할 때, DTO의 생성자 파라미터에 정확히 일치하는 개수와 순서대로 값을 할당해야 합니다. likedByMe 필드는 기본값이 false로 설정되어 있었지만, selectNew는 기본값을 무시하고 모든 파라미터에 값을 전달하도록 강제합니다. 이로 인해 초기 코드에서 오류가 발생했습니다.

해결 방법

이 문제를 해결하기 위해 likedByMe 필드를 생성자 밖으로 이동하여 기본값을 설정하도록 변경했습니다. 수정된 DTO는 다음과 같습니다.

@Schema(description = "서클 페이지 조회 응답")
data class CirclePageResponse(
    val id: UUID,
    val thumbnailUrl: String? = null,
    val title: String,
    val introduction: String = "",
    val leaderProfile: String? = null,
    val leaderName: String,
    val englishLevel: EnglishLevel,
    val city: City,
    val capacity: Int,
    val totalView: Int,
    val totalLike: Int
) {
    val likedByMe: Boolean = false
}

이제 selectNew는 DTO의 모든 생성자 파라미터에 값을 할당할 필요가 없어졌습니다. likedByMe 필드는 클래스 바디에 정의되어 기본값 false로 설정됩니다. 이로써 selectNew의 제한 사항을 우회하면서 원하는 데이터를 매핑할 수 있었습니다.

'Kotlin-Jdsl' 카테고리의 다른 글

Kotlin JDSL에서 Null 값을 가진 조건 무시하기  (1) 2024.11.07

들어가며

React에서 "A component is changing an uncontrolled input to be controlled"*라는 경고 메시지를 만난 적이 있나요? 이 에러는 보통 input 요소가 uncontrolled 상태로 시작했다가, 이후 controlled 상태로 변환될 때 발생합니다. 이러한 경고는 개발 환경에서만 표시되지만, 무시하고 넘어가면 앱의 일관성과 안정성에 문제가 생길 수 있습니다.

이번 글에서는 이 문제를 해결하는 방법을 예시 코드와 함께 알아보겠습니다.


Uncontrolled와 Controlled Input 차이

Uncontrolled input은 React가 그 값을 관리하지 않는 입력 요소를 의미합니다. 사용자가 입력한 값을 직접적으로 DOM이 관리하고, React의 상태로부터 독립적인 상태로 동작합니다. 반대로 Controlled input은 React 상태에 의해 관리되는 입력 요소로, 사용자가 입력한 값이 항상 React의 state에 저장되며 상태에 따라 값이 동적으로 변화합니다.

문제는 다음과 같은 상황에서 발생합니다:

  • input 필드가 처음에 undefined 또는 null 값으로 설정되어 uncontrolled로 시작된 후, 나중에 controlled 상태로 변경되는 경우입니다.

경고 메시지

에러 메시지는 다음과 같은 형태로 나타납니다:

Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen.
 

이러한 경고는 필드의 value가 처음에 정의되지 않았기 때문에 uncontrolled로 시작하고, 이후에 정의된 값이 할당되면서 controlled 상태로 전환될 때 발생합니다.


문제 해결 방법

문제를 해결하려면 input 필드의 value를 항상 정의된 값으로 초기화해야 합니다. 즉, 빈 문자열('')을 사용하여 controlled 상태를 유지하는 것이 중요합니다.

예를 들어, 다음과 같은 코드를 살펴보겠습니다:

useEffect(() => {
  if (!profile) {
    setProfile({
      name: '',
      nickname: '',
      email: '',
      password: '',
      newPassword: '',
      confirmNewPassword: '',
      profileImage: '', // 빈 문자열로 초기화
      isMarketingAgreed: false // 기본값 false
    })
  }
}, [])
 

위 코드는 profile 값이 undefined일 경우, setProfile을 호출하여 빈 문자열로 모든 필드를 초기화하는 방법입니다. 이렇게 함으로써 React는 input 요소를 항상 controlled 상태로 유지할 수 있습니다.

이번 주에는 마이 페이지에서 사용자의 프로필 사진을 업데이트하는 기능을 개발하게 되었습니다. 이미지를 저장하기 위해 여러 방법을 고려했는데, 그중 로컬 서버 대신 안정적이고 확장성이 높은 클라우드 스토리지인 AWS S3를 선택하게 되었습니다. AWS S3를 사용하는 방법에 대한 글은 많이 찾아볼 수 있지만, 이번 글에서는 S3를 사용하면서 마주쳤던 문제들과 그 해결 과정에 대해 회고해보려 합니다.

S3 접근 권한 설정

S3 버킷에 파일을 업로드하려면 먼저 해당 버킷에 접근할 수 있는 권한이 필요합니다. 이때 엑세스 키시크릿 키라는 두 개의 키가 사용됩니다. 키는 AWS에서 직접 발급할 수 있지만, 보안상 매우 중요한 문제입니다. 만약 키가 외부로 노출된다면, 누군가가 이를 악용해 우리가 모르는 사이 비트코인 채굴 등의 작업을 수행하거나, 막대한 서버 비용을 초래할 수 있습니다.

따라서 절대로 AWS Root 계정으로 발급된 키를 사용해서는 안 됩니다. 대신, S3에만 접근할 수 있는 최소한의 권한을 가진 사용자를 별도로 만들어 그 키를 사용하는 것이 안전합니다. 이를 위해 AWS의 Identity and Access Management(IAM)에서 사용자 그룹을 생성하고, 해당 그룹에 AmazonS3FullAccess 권한을 할당합니다. 이후, 그 그룹에 속한 사용자에게만 엑세스 키를 발급하면, S3 버킷에 대한 안전한 접근이 가능합니다.

IAM에서 권한을 부여하고 키를 발급하는 과정은 다음과 같습니다:

  1. AWS 관리 콘솔에서 IAM 대시보드로 이동합니다.
  2. 사용자 그룹을 생성하고, AmazonS3FullAccess 권한을 해당 그룹에 할당합니다.
  3. 해당 그룹에서 사용자를 생성하고, 그 사용자에게 엑세스 키를 발급합니다.

이제, 이 엑세스 키와 시크릿 키를 사용하여 S3에 접근할 수 있게 됩니다.

GitHub에서의 보안 관리

S3 접근 키를 발급받았다면, 이를 서버에서 안전하게 사용할 방법을 고민해야 합니다. 특히 GitHub에 소스 코드를 올릴 때는 더욱 주의가 필요합니다. 퍼블릭 레포지토리에 키가 노출되면 외부에서 악용될 가능성이 높습니다. 이를 방지하기 위해 GitHub의 Secrets 기능을 활용해 환경 변수를 설정할 수 있습니다. 이렇게 하면 키가 소스 코드에 포함되지 않고도 안전하게 사용할 수 있습니다.

로컬 환경에서는 application.yml 파일을 사용해 키를 관리하는 것이 일반적입니다. 그러나 application.yml 파일이 git ignore에 등록되지 않으면, 실수로 이 파일이 퍼블릭 레포지토리에 노출될 수 있습니다. 이를 방지하기 위해, application-local.yml, application-dev.yml, application-prod.yml처럼 환경별로 파일을 분리하고, application-prod.yml만 실제 서버에 배포하도록 설정하는 것이 좋습니다. 이 방법은 Spring Boot를 사용하는 개발자들에게 특히 유용합니다.

S3 파일 업로드 기능 구현

이제 S3 버킷에 접근하기 위한 설정을 마쳤으니, 본격적으로 파일을 업로드하는 기능을 구현해보겠습니다. 우선, 필요한 의존성을 추가해야 합니다.

Gradle 의존성 추가

S3 파일 업로드를 위해 AWS SDK를 의존성에 추가해야 합니다. Kotlin과 Spring Boot 환경에서 다음과 같이 의존성을 추가할 수 있습니다.

implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")​
 

application.yml 설정

application.yml 파일에 S3와 관련된 설정 정보를 입력합니다. 여기에서 stack.auto를 false로 설정하여 Spring Boot가 AWS 인프라를 자동으로 구성하지 않도록 해야 합니다. 또한, region.static 값을 ap-northeast-2로 설정하여 S3 버킷의 리전을 서울로 지정합니다.

cloud:
  aws:
    s3:
      bucket: 본인의 버킷 명
    stack.auto: false
    region.static: ap-northeast-2
    credentials:
      accessKey: 본인의 엑세스 키
      secretKey: 본인의 시크릿 키

S3Config 설정 클래스

S3에 접근하기 위한 클라이언트를 설정하는 S3Config 클래스를 작성합니다. 이 클래스는 S3 클라이언트를 Spring 애플리케이션에서 빈으로 등록하여 다른 곳에서 재사용할 수 있게 해줍니다.

@Configuration
class S3Config(
        @Value("\${cloud.aws.credentials.access-key}") private val accessKey: String,
        @Value("\${cloud.aws.credentials.secret-key}") private val secretKey: String,
        @Value("\${cloud.aws.region.static}") private val region: String
) {

    @Bean
    fun amazonS3Client(): AmazonS3Client {
        val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(AWSStaticCredentialsProvider(awsCredentials))
                .build() as AmazonS3Client
    }
}

파일 업로드 서비스

S3 버킷에 파일을 업로드하기 위한 서비스 클래스를 작성합니다. 이 클래스에서는 파일을 업로드하고, 업로드된 파일의 URL을 반환하는 기능을 구현합니다.

@Service
class S3ImageUploadService(
        private val amazonS3Client: AmazonS3
) {

    @Value("\${cloud.aws.s3.bucket}")
    private lateinit var bucket: String

    @Throws(IOException::class)
    fun uploadFile(file: MultipartFile): String {
        val fileName = generateFileName(file.originalFilename!!)
        val metadata = ObjectMetadata()
        metadata.contentLength = file.size

        amazonS3Client.putObject(bucket, fileName, file.inputStream, metadata)
        return amazonS3Client.getUrl(bucket, fileName).toString() // 업로드된 파일의 URL 반환
    }

    private fun generateFileName(originalFilename: String): String {
        val fileExtension = originalFilename.substringAfterLast(".")
        return "${UUID.randomUUID()}.$fileExtension"
    }
}

마무리

이 글을 통해 AWS S3를 사용하여 이미지를 업로드하는 방법과, 그 과정에서 발생할 수 있는 보안 문제에 대해 다루었습니다. 특히 IAM 권한 관리GitHub 보안 설정에 대한 부분은 꼭 주의해야 할 사항입니다. 이를 통해 AWS S3를 효율적이고 안전하게 사용할 수 있기를 바랍니다.

'스프링' 카테고리의 다른 글

@Entity  (0) 2022.02.01
spring boot 에서 google login을 사용해보았다.  (0) 2021.12.18
Spring 웹 계층  (0) 2021.12.14
스프링 서버 단에서 데이터 처리하는 방식  (0) 2021.11.27
Auditing @CreatedDate @LastModifiedDate  (0) 2021.11.27

이번에 새로운 사이드 프로젝트를 진행하면서 Kotlin, JPA, Spring Boot를 활용하게 되었다. 관련 예제 코드를 찾아보니, Kotlin과 JPA로 작성된 Entity 클래스는 보통 아래와 같은 형태로 많이 작성된다.

 

@Entity
data class Article(
    var slug: String = "",
    var title: String = "",
    var description: String = "",
    var body: String = "",
    @ManyToMany
    val tagList: MutableList<Tag> = mutableListOf(),
    var createdAt: OffsetDateTime = OffsetDateTime.now(),
    var updatedAt: OffsetDateTime = OffsetDateTime.now(),
    @ManyToMany
    var favorited: MutableList<User> = mutableListOf(),
    @ManyToOne
    var author: User = User(),
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long = 0
) {
    fun favoritesCount() = favorited.size
}

Kotlin의 data class에서는 생성자에 var와 val 키워드를 사용하면, 생성자 파라미터와 클래스 변수를 동시에 선언할 수 있다. 이를 통해 작성해야 할 코드 양을 줄일 수 있는 장점이 있다. 그러나 이 방식은 엔티티의 값을 외부에서 쉽게 변경할 수 있는 가능성을 열어두기 때문에, 좋은 설계라고 볼 수 없다. 그렇다면, 어떻게 엔티티 클래스를 작성하는 것이 더 나은 방식일까?

 

var와 val의 차이점

Kotlin에서 var는 변수의 값을 변경할 수 있게 만들고, val은 초기화된 후 값을 변경할 수 없도록 만든다. 따라서, 변경되지 않아야 할 필드에는 val을 사용하고, 외부에서 변경이 가능한 필드는 var를 사용하게 된다.

변경이 불필요한 필드는 val을 사용하여 불변성을 유지하는 것이 좋지만, JPA Entity에서는 모든 필드를 val로 선언할 수는 없다. 예를 들어, 엔티티의 식별자(id)는 데이터베이스에서 자동 생성되므로, 코드에서 변경할 수 없게 막을 수 없다. 또한, JPA는 기본 생성자와 함께 동작하는 특성이 있으므로, data class보다는 일반 클래스를 사용하는 것이 더 적합할 때가 많다.

개선된 설계1: 불변성을 고려한 엔티티 작성

아래는 JPA와 Kotlin에서 엔티티 설계를 개선한 예시다. var를 최소화하고, setter는 외부에서 호출할 수 없도록 protected로 제한하여 불필요한 변경을 방지한다.

@Entity
class Article(
    slug: String = "",
    title: String = "",
    description: String = "",
    body: String = "",
    @ManyToMany
    val tagList: MutableList<Tag> = mutableListOf(),
    createdAt: OffsetDateTime = OffsetDateTime.now(),
    updatedAt: OffsetDateTime = OffsetDateTime.now(),
    @ManyToOne
    val author: User = User()
) {
    var slug: String = slug
        protected set
    var title: String = title
        protected set
    var description: String = description
        protected set
    var createdAt: OffsetDateTime = createdAt
        protected set
    var updatedAt: OffsetDateTime = updatedAt
        protected set

    @ManyToMany
    var favorited: MutableList<User> = mutableListOf()
        protected set

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long = 0
        protected set

    fun favoritesCount() = favorited.size
}

이 방식은 외부에서 함부로 필드 값을 변경하지 못하게 막으면서, 엔티티의 불변성을 유지할 수 있다. slug, title, description 등은 외부에서 접근은 가능하지만, 값을 변경할 수는 없다. 대신, 비즈니스 로직 내에서 값이 필요할 때만 변경할 수 있다. 하지만 val을 사용하는 변수만 클래스의 생성자에 두는 것은 코드의 일관성이 떨어지게 만들 수 있다 생각한다.

 

개선된 설계2: 모든 필드를 클래스 내부에서 선언하기

아래는 val과 var 모두를 생성자가 아닌 클래스 내부에 선언하여 관리하는 방식의 예시다. 이 방식은 코드의 구조를 더 명확하게 하고, 엔티티의 불변성을 강력하게 유지할 수 있다.

@Entity
class Article(
    slug: String = "",
    title: String = "",
    description: String = "",
    body: String = "",
    tagList: MutableList<Tag> = mutableListOf(),
    createdAt: OffsetDateTime = OffsetDateTime.now(),
    updatedAt: OffsetDateTime = OffsetDateTime.now(),
    author: User = User()
) {
    var slug: String = slug
        protected set
    var title: String = title
        protected set
    var description: String = description
        protected set
    var body: String = body
        protected set

    @ManyToMany
    val tagList: MutableList<Tag> = tagList
        protected set

    var createdAt: OffsetDateTime = createdAt
        protected set
    var updatedAt: OffsetDateTime = updatedAt
        protected set

    @ManyToMany
    var favorited: MutableList<User> = mutableListOf()
        protected set

    @ManyToOne
    var author: User = author
        protected set

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long = 0
        protected set

    fun favoritesCount() = favorited.size
}

위와 같이 모든 필드를 클래스 내부에서 선언하면, 외부에서 접근할 수 있는 경로를 명확히 제한할 수 있다. 불필요한 수정이 불가능해지며, 엔티티의 일관성이 더욱 강화된다.

 

 

리눅스 서버를 운영하면서 로그 분석은 매우 중요한 작업 중 하나입니다. 서버의 상태를 모니터링하고, 문제를 신속하게 해결하기 위해서는 로그 파일을 효율적으로 분석할 수 있어야 합니다. 이번 글에서는 로그 분석에 유용한 주요 리눅스 명령어들을 정리해보았습니다.

1. grep – 텍스트 검색의 기본

로그 분석의 시작은 원하는 정보가 포함된 라인을 찾아내는 것입니다. grep 명령어는 특정 패턴을 포함한 텍스트를 파일에서 검색하는 데 사용됩니다.

  • 기본 사용법:
grep "검색할 패턴" 파일명
  • 예시:
grep "userId=" 7827540

위 명령어는 7827540 파일에서 userId=를 포함하는 모든 라인을 출력합니다.

유용한 옵션들

  • -i: 대소문자를 구분하지 않고 검색
  • -r 또는 -R: 디렉토리 내의 파일들을 재귀적으로 검색
  • -n: 검색된 라인의 번호를 함께 출력
  • --color=auto: 매칭된 부분을 색깔로 강조하여 보여줍니다.

2. awk – 텍스트 처리의 강력한 도구

awk는 로그에서 특정 필드를 추출하거나 조작하는 데 유용한 명령어입니다. 특히, grep과 함께 사용하여 필터링된 결과에서 필요한 부분만 선택할 수 있습니다.

  • 기본 사용법:
awk '{ print $필드번호 }' 파일명
  • 예시:
grep "userId=" 7827540 | awk '{ print $3 }'

이 명령어는 grep으로 필터링한 후, 세 번째 필드만 출력합니다. 필드에 대한 설명을 좀 더 해보겠습니다.
예를 들어, 다음과 같은 로그 라인이 있다고 가정해 보겠습니다:

2024-08-30 12:34:56 INFO userId=12345 action=login status=success

이 라인을 awk로 처리한다고 할 때:

  • 첫 번째 필드는 2024-08-30
  • 두 번째 필드는 12:34:56
  • 세 번째 필드는 INFO
  • 네 번째 필드는 userId=12345

이런 식으로, 공백을 기준으로 라인이 분리되고 각 부분이 필드로 간주됩니다. 따라서 awk '{ print $3 }'는 이 라인에서 INFO를 출력하게 됩니다.

3. cat – 파일 내용 출력

cat은 파일의 내용을 터미널에 출력하는 명령어입니다. 단순히 파일을 읽거나, 여러 파일을 연결하여 출력하는 데 사용됩니다.

  • 기본 사용법:
cat 파일명
  • 예시:
cat 7827540

이 명령어는 7827540 파일의 모든 내용을 출력합니다.

4. less – 대용량 파일 탐색

로그 파일이 클 때는 less 명령어가 유용합니다. less는 파일 내용을 페이지 단위로 출력하여 한 번에 보기 편하게 해줍니다.

  • 기본 사용법:
less /var/log/syslog
  • 예시:
less 파일명

이 명령어로 syslog 파일을 페이지 단위로 탐색할 수 있으며, q 키를 눌러 종료할 수 있습니다.

5. tail – 실시간 로그 모니터링

tail 명령어는 파일의 마지막 몇 줄을 출력합니다. 특히, 실시간으로 로그를 모니터링하고자 할 때 유용합니다.

  • 기본 사용법: 
tail -f /app/logs/tomcat/tsolution.log

실시간 로그 확인:

tail -f 파일명

예시:

tail -f /app/logs/tomcat/tsolution.log

이 명령어는 tsolution.log 파일의 마지막 부분을 실시간으로 출력합니다.

6. find – 파일 검색

find 명령어는 특정 조건에 맞는 파일을 검색하는 데 사용됩니다. 예를 들어, 로그 파일의 위치를 찾을 때 유용합니다.

  • 기본 사용법:
find 디렉토리 -name "파일명"

예시:

find /app/logs/ -name "tsolution.2024-07*"

이 명령어는 /app/logs/ 디렉토리 내에서 tsolution.2024-07로 시작하는 파일들을 검색합니다.

이처럼 리눅스의 다양한 명령어를 활용하면 로그 분석 작업을 훨씬 더 효율적으로 수행할 수 있습니다. 명령어들 간의 조합과 파이프(|)를 활용한 유연한 분석 방법을 익히면, 로그 파일에서 필요한 정보를 빠르게 추출하고 문제를 해결하는 데 큰 도움이 될 것입니다.

이 글이 리눅스 로그 분석을 시작하는 데 유용한 가이드가 되기를 바랍니다!

웹 개발에서 HTTP 요청을 보내기 위한 도구는 필수적입니다. 두 가지 대표적인 방법은 브라우저에 내장된 Fetch API와 추가 라이브러리인 Axios입니다. 이 글에서는 Axios와 Fetch API를 비교하여, 어떤 상황에서 어떤 도구를 선택하는 것이 적절한지 알아보겠습니다.

 

1. Fetch API란?

Fetch API는 브라우저에 내장된 API로, HTTP 요청을 보내고 응답을 처리하는 표준 방법입니다. Promise 기반으로 비동기 작업을 처리하며, ES6 이상을 지원하는 모든 현대 브라우저에서 사용할 수 있습니다.

Fetch API의 주요 특징

  • 내장 API: 별도의 설치 없이 바로 사용 가능.
  • Promise 기반: 비동기 작업을 처리하기에 적합하며, async/await과 함께 사용하기 편리합니다.
  • 유연성: 다양한 HTTP 메서드(GET, POST, PUT, DELETE 등)를 쉽게 사용할 수 있습니다.

Fetch API 사용 예시

async function getUserData() { 
	const response = await fetch('https://api.example.com/user'); 
    if (!response.ok) { 
    	throw new Error('네트워크 응답에 문제가 있습니다.'); 
    } 
	const data = await response.json(); return data; 
}
 

Fetch API의 장단점

장점

  1. 내장 API: 브라우저에 내장되어 있어 별도의 설치나 설정이 필요 없습니다.
  2. 유연한 사용: 단순한 HTTP 요청부터 복잡한 요청까지 다양한 방식으로 활용 가능합니다.

단점

  1. 오류 처리의 복잡성: HTTP 상태 코드가 200번대가 아닐 경우에도 오류를 발생시키지 않기 때문에, 추가적인 오류 처리 로직이 필요합니다.
  2. 기능 부족: 요청 취소, 응답 데이터 변환 등 고급 기능을 구현하려면 추가 코드가 필요합니다.

2. Axios란?

Axios는 Promise 기반의 HTTP 클라이언트 라이브러리로, 브라우저와 Node.js 환경 모두에서 사용할 수 있습니다. Axios는 fetch보다 더 많은 기능을 제공하며, 그 사용성과 편리성으로 인해 많은 개발자들 사이에서 인기가 높습니다.

Axios의 주요 특징

  • 자동 JSON 변환: 응답 데이터를 자동으로 JSON 형식으로 변환합니다.
  • 요청 및 응답 인터셉터: 요청이나 응답을 가로채고 처리할 수 있는 기능을 제공합니다.
  • 취소 가능한 요청: axios.CancelToken을 사용해 요청을 취소할 수 있습니다.
  • 브라우저와 Node.js 지원: 클라이언트와 서버 측 모두에서 동일한 API로 사용 가능합니다.

Axios 사용 예시

import axios from 'axios'; 

async function getUserData() {
	try { 
    	const response = await axios.get('https://api.example.com/user');
        return response.data; 
    } catch (error) { 
   	 throw new Error('데이터를 가져오는 중 오류가 발생했습니다.'); 
    } 
}

 

Axios의 장단점

장점

  1. 간편한 오류 처리: HTTP 상태 코드가 200번대가 아닐 경우 자동으로 오류를 발생시킵니다.
  2. 고급 기능 지원: 요청 및 응답 인터셉터, 요청 취소, 자동 JSON 변환 등 다양한 고급 기능을 제공합니다.
  3. 브라우저와 Node.js 지원: 동일한 코드로 브라우저와 서버 환경에서 모두 사용 가능합니다.

단점

  1. 추가 설치 필요: 라이브러리를 설치하고 의존성을 관리해야 합니다.
  2. 용량: Fetch API보다 약간 더 많은 용량을 차지합니다.

3. Fetch API와 Axios의 비교

3.1. 사용성

Fetch API는 내장된 API로 간단한 요청을 처리할 때 직관적이고 사용하기 쉽습니다. 하지만 복잡한 오류 처리나 요청 취소 같은 고급 기능을 구현하려면 추가적인 코드가 필요합니다.

Axios는 이런 고급 기능을 기본으로 제공하여, 더 복잡한 요구사항을 쉽게 처리할 수 있도록 해줍니다. 특히, RESTful API와의 상호작용에서 반복적인 작업을 줄여주는 인터셉터 기능이 유용합니다.

3.2. 오류 처리

Fetch는 상태 코드가 200번대가 아니더라도 오류를 발생시키지 않기 때문에, 응답을 직접 검사하고 오류를 처리해야 합니다. 반면 Axios는 상태 코드에 따라 자동으로 오류를 발생시켜, 오류 처리가 더 간편합니다.

3.3. 브라우저 호환성 및 환경 지원

Fetch는 최신 브라우저에서 기본적으로 지원되지만, 오래된 브라우저에서는 폴리필이 필요할 수 있습니다. Axios는 브라우저뿐만 아니라 Node.js 환경에서도 잘 작동합니다. Axios는 서버와 클라이언트 양쪽에서 동일한 API로 코드를 작성할 수 있는 장점이 있습니다.

3.4. 기능 확장성

Axios는 기본적으로 JSON 변환, 요청 취소, 헤더 설정, 요청 및 응답 인터셉터 등의 고급 기능을 제공하므로, 보다 복잡한 HTTP 요청을 처리할 때 유리합니다. Fetch API로 이러한 기능을 구현하려면 추가 코드가 필요합니다.

4. 어떤 것을 선택해야 할까?

단순한 프로젝트나 기본적인 요청이 필요한 경우: Fetch API가 충분합니다. 내장 API이기 때문에 추가적인 의존성 없이 간단하게 사용할 수 있습니다.

고급 기능이 필요하거나 대규모 프로젝트인 경우: Axios가 더 적합합니다. 특히, 복잡한 요청 로직이나 여러 API 요청이 필요하다면 Axios의 기능들이 큰 도움이 될 것입니다.

서버 측 코드와 클라이언트 측 코드가 모두 필요한 경우: Axios는 브라우저와 Node.js 환경 모두에서 일관된 API를 제공하므로, 동일한 코드로 클라이언트와 서버를 처리할 수 있습니다.

결론

Fetch API와 Axios는 모두 HTTP 요청을 처리하기 위한 훌륭한 도구입니다. 선택은 프로젝트의 요구사항과 개발 환경에 따라 달라질 수 있습니다. 단순하고 가벼운 프로젝트라면 Fetch API로 충분하지만, 복잡한 기능이나 더 나은 오류 처리가 필요하다면 Axios가 더 나은 선택일 수 있습니다. 프로젝트의 특성을 잘 이해하고 그에 맞는 도구를 선택하는 것이 중요합니다.

 

데이터베이스 작업을 하다 보면, 종종 여러 행에 있는 데이터를 하나의 문자열로 결합하여 표시해야 할 때가 있습니다. 예를 들어, 동일한 그룹의 모든 항목 이름을 하나의 열에 표시하고 싶을 때가 있습니다. MySQL에서 이 작업을 쉽게 수행할 수 있는 방법은 바로 GROUP_CONCAT 함수를 사용하는 것입니다.

GROUP_CONCAT 함수란?

GROUP_CONCAT 함수는 MySQL에서 제공하는 집계 함수 중 하나로, 그룹화된 데이터에서 여러 값을 하나의 문자열로 결합해 줍니다. 이 함수는 GROUP BY와 함께 사용되며, 여러 행에 걸쳐 있는 값을 하나의 결과로 만들어 줍니다. 결합된 값들은 기본적으로 쉼표(,)로 구분되지만, 다른 구분자를 사용할 수도 있습니다.

기본 사용법

GROUP_CONCAT 함수의 기본 구문은 다음과 같습니다:

GROUP_CONCAT(
  [DISTINCT] column_name 
  [ORDER BY column_name ASC|DESC] 
  [SEPARATOR 'separator_string']
  )
  • DISTINCT: 중복된 값을 제거하고 싶을 때 사용합니다.
  • ORDER BY: 결합할 값을 정렬할 때 사용합니다. 기본적으로 오름차순(ASC)으로 정렬됩니다.
  • SEPARATOR: 결합된 값 사이에 사용할 구분자를 지정합니다. 기본값은 쉼표(,)입니다.

실습 예제: 클래스 이름 결합하기

이제 실제 예제를 통해 GROUP_CONCAT의 사용법을 살펴보겠습니다. 예를 들어, 특정 회사에서 제공하는 워크아웃 클래스의 이름을 하나의 열에 모아서 표시하고 싶다고 가정해 보겠습니다.

SELECT 
  ca.goods_id, 
  GROUP_CONCAT(DISTINCT wc.name ORDER BY wc.name SEPARATOR ', ') AS class_names 
FROM class_authority ca 
LEFT JOIN workout_class wc ON wc.id = ca.class_id 
WHERE wc.company_id = 296 
GROUP BY ca.goods_id;

쿼리 설명

  • ca.goods_id: 클래스와 연결된 상품 ID입니다. 이 필드를 기준으로 그룹화하여 각 상품에 대한 결과를 하나의 행으로 표시합니다.
  • GROUP_CONCAT(DISTINCT wc.name ORDER BY wc.name SEPARATOR ', '): 동일한 goods_id에 속하는 클래스 이름(wc.name)을 중복 제거 후 알파벳 순서대로 정렬하여 쉼표로 구분된 문자열로 결합합니다.
  • LEFT JOIN workout_class wc ON wc.id = ca.class_id: class_authority 테이블과 workout_class 테이블을 조인하여 각 상품 ID에 해당하는 클래스를 가져옵니다.
  • WHERE wc.company_id = 296: 특정 회사(예: company_id = 296)에 속하는 클래스만 선택합니다.
  • GROUP BY ca.goods_id: goods_id를 기준으로 결과를 그룹화하여, 각 상품에 대해 하나의 행만 반환합니다.

결과 예시

위 쿼리를 실행하면 다음과 같은 결과를 얻을 수 있습니다:

101 Yoga, Zumba, Pilates
102 Body Pump, CrossFit, Spin
103 Kickboxing, Muay Thai, Karate

여기서 goods_id = 101에 대한 클래스 이름이 "Yoga, Zumba, Pilates"로 결합되어 표시됩니다. 각 클래스 이름은 알파벳 순서로 정렬되어 쉼표로 구분되었습니다.

결론

GROUP_CONCAT 함수는 데이터를 집계하고 결합하는 데 매우 유용한 도구입니다. 이 함수는 특히 여러 값을 하나의 열에 결합하여 표시해야 할 때 유용하며, 데이터베이스 내에서 간단한 텍스트 조작을 할 수 있게 해줍니다. GROUP_CONCAT을 잘 활용하면 데이터베이스 쿼리를 더욱 강력하게 만들 수 있습니다.

이제 여러분도 GROUP_CONCAT을 사용하여 데이터를 그룹화하고, 필요에 따라 원하는 방식으로 결합해 보세요!

'Database' 카테고리의 다른 글

기존 테이블에 속성 추가  (0) 2021.11.11
동적 피벗 테이블  (0) 2021.01.27
mssql - ISNULL, IN, Procedure  (0) 2021.01.18
mssql - join, between, like '%'  (0) 2021.01.18

 

에러 상세 내역

e: file:///C:/Users/TUF/workspace/together-english-backend/src/main/kotlin/com/together_english/deiz/data/member/entity/Member.kt:23:5 Platform declaration clash: The following declarations have the same JVM signature (getPassword()Ljava/lang/String;):
    fun `<get-password>`(): String defined in cohttp://m.together_english.deiz.data.member.entity.Member
    fun getPassword(): String defined in cohttp://m.together_english.deiz.data.member.entity.Member

 

오랫만에 프로젝트를 실행해보니 위와같은 에러를 만나게 되었다. 확인해보니 UserDetails의 getPassword 메소드를 오버라이딩해야되기 때문에 다음과같이 선언했던 password를 return 하도록 만들었다.

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    var password: String = password
        private set
        
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    override fun getPassword(): String {
        return this.password
    }

코틀린에서는 변수의 getter와 setter를 자동으로 만들어주는 기능이 있기 때문에 password라는 변수가있으면 자동으로 getPassword 메소드를 만들게 된다. 하지만 이와 같은 이름의 메소드를 구현하였기 때문에 충돌하여 발생한 에러였다. 

 

해결책

UserDetails 인터페이스를 상속받아 getPassword 메소드를 오버라이딩 해야되기 때문에 해당 메소드의 명은 변경할 수 없다. 그렇기 때문에 password의 변수 명을 변경하여 해당 문제를 해결할 수 있다.

+ Recent posts