프로젝트 개요

자사 메인 홈페이지는 기존에 운영되고 있었으나, 협력 업체와의 계약 및 유지보수 문제로 인해 지속적인 업데이트가 불가능한 상황이 발생하였습니다. 또한, 기존 홈페이지는 다음과 같은 주요 문제점을 포함하고 있었습니다.

  • 의학적 용어 필터링 미비: 사용자에게 혼란을 줄 수 있는 의학적 용어에 대한 필터링 기능이 부재하여 정보 전달의 정확성이 저하됨
  • 다국어 지원 문제: 글로벌 사용자 대상의 다국어 기능이 정상적으로 동작하지 않아 사용자 경험(UX) 저하
  • 기타 운영상 문제: 유지보수 한계 및 최신 기술 적용의 어려움으로 인해 보안성과 확장성이 부족

이에 따라, 기존 홈페이지의 문제점을 해결하고, 보다 효율적인 운영이 가능한 구조로 개편하기 위해 홈페이지 리뉴얼을 진행하였습니다.

프로젝트 소개

  • 이모티브 홈페이지: 자사의 연혁과 서비스, 협력사, 등을 소개하는 서비스
  • Link : 이모티브 홈페이지
  • 인원 : FE(1), BE(1), UI(1), UX(1)
  • 기술 스택: Kotlin, MySQL, Redis, Spring Boot, AWS S3, Swagger UI, JPA, AWS Codepipeline, JWT
  • 기간 : 2024.01 - 2024.03
  • 서버 스팩 : AWS EC2 t2.micro (CPU 1 Core, RAM 1GB), RDS(MySQL db.t2.micro)

기술 상세

배포를 빠르고 안정적으로: AWS CodePipeline을 이용한 CI/CD 자동화 배포

AWS CodePipeline 구조도

architecture

  • 도입 배경
    • 회사의 홈페이지는 업데이트 다소 주기가 짧을 수 있고, 코드 수정 시 발생하는 빌드, 테스트, 배포를 하는 것이 시간이 많이 소요되는 비효율적인 작업이었기 때문에 AWS CodePipeline을 통한 CI/CD 파이프라인을 자동화 하였습니다.
    • AWS EC2를 이용하여 서버를 구성하기에 Jenkins와 같은 자체 관리 서비스에 비해 AWS CodePipeline을 이용하면 인프라 관리 비용이 절감되기에 부담이 적어 AWS CodePipeline을 도입하였습니다.
    • AWS S3를 이용하여 아티팩트를 버전별로 관리하고 오류 시 빠르고 쉬운 복구 및 이전 버전으로의 롤백이 가능하기에 실제 업무에서 발생할 수 있는 이슈에 빠르게 대응할 수 있었습니다.
  • 사용 이유
    • 이모티브에서는 스마일샤크(AWS 빌링 시스템 업체)와의 계약을 진행중이었고 또한 다수의 프로젝트들 AWS를 사용 중이었습니다. 또한 AWS 웹 콘솔에서 직관적으로 파이프라인을 관리할 수 있으며, IAM을 통한 권한 관리보안 설정 관리가 용이합니다.
    • 배포 과정에서 발생할 수 있는 휴먼 에러를 줄일 수 있으며, Jenkins, TeamCity와 같은 별도의 Ci/CD 서버를 운영할 경우, 인프라 운영 무담이 존재하지만, AWS의 관리형 서비스인 CodePipeline을 이용하면, 이러한 운영 부담을 제거할 수 있습니다.
  • 성과
    • 코드 변경 즉시 빌드/테스트/배포가 자동 진행되어 개발 및 배포 속도가 향상되었습니다.
    • 관리형 서비스 사용으로 운영 비용 감소되었고 효율적으로 리소스를 사용할 수 있었습니다.
    • 배포 과정 및 배포 후 문제가 발생하여도 빠르게 이전 버전으로 롤백하거나 수정 배포 가능해졌습니다.
    • 개발자는 코드 품질 및 신규 기능 개발에 집중할 수 있어 팀 생산성이 향상되었습니다.

정확한 번역을 위해: MessageSource를 이용한 다국어 처리

  • 도입 배경
    • 자사 홈페이지의 기능 상, 정적 콘텐츠(인사말, 서비스 소개, 회사 소개, 연혁 등)이 많이 사용되기 때문에 Google API, 또는 Papago API와 같은 외부 연동 API의 사용은 비용과 서버 성능적으로 불필요한 기능이라고 생각하였습니다. 또한 의학 용어가 많이 사용될 수 있음을 고려하여 번역의 정확가 중요하며 단어 하나가 법적인 문제로 직결될 수 있음에 외부 연동보다는 번역의 정확도를 올리는 것에 초점을 맞춰 MessageSource를 도입하였습니다.
  • 사용 이유
    • 의학 용어 번역의 정확도와 오역이 크리티컬 할 수 있기 때문에 미리 검수된 메세지를 사용하는 방식으로 사용할 수 있습니다.
    • 회사의 홈페이지의 경우 렌더링 속도가 중요하였고, MessageSource를 이용한 방식으로 빠른 응답과 안정성을 확보할 수 있습니다.
    • API를 사용하면 비용이 추가적으로 계속 발생할 수 있으나, MessageSource는 초기 구축 이후에 추가 비용이 발생하지 않습니다.
    • 회사의 브랜딩의 초석을 다지는 것은 회사의 홈페이지에서 부터 시작하기 때문에 브랜드 용어 관리 측면에서 미리 정의된 메세지를 관리하는 것에 용이합니다.
  • 성과
    • 외부 번역 API를 사용하지 않고 구축할 수 있어 비용이 절감되었습니다.
    • 네트워크 통신 없이 내부에서 바로 메시지를 로딩하여 API 번역기 이용 대비 응답 속도를 개선할 수 있었습니다: 800ms -> 15ms
    • 추가적인 컨텐츠(구성원 추가/삭제 및 인터뷰 등)이 발생하여도 즉각적인 대응이 가능하였습니다.
    • 번역의 확장이 가능하여 비즈니스가 확장됨에 따른 번역 요구 사항이 증가하여도 빠르게 대응할 수 있었습니다.

실시간 번역: Database MessageSource & JPA

  • 도입 배경
    • 단순한 MessageSource를 이용해서는 메세지 추가 또는 변경 시, 서버 재배포 재시작이 필요한 한계점이 존재하게 되었습니다.
    • 언어별로 콘텐츠가 많아질수록 message properties의 관리 비용이 증가함에 따라 어려움이 발생하였습니다.
    • 또한 인터뷰, 구성원 추가 등의 기능 발생 시, 기존 properties의 설정을 재사용할 수 없어 재사용성이 떨어지게 되었습니다.
  • 사용 이유
    • 홈페이지 운영시, 운영팀에서 구성원 추가 및 인터뷰, 회사 연혁 및 이벤트 추가 시 관리를 편리하게 관리할 수 있습니다.
    • 언어 추가 및 콘텐츠 확장이 가능하여 비즈니스 추가 요구사항이 증가하더라도 쉽게 관리가 가능하므로 유지보수 업무가 감소할 수 있습니다.
    • 서버의 재배포 없이 실시간으로 대응 가능하도록 하여 운영의 편의성을 높이고자 하였습니다.
  • 추가 사항
    • MyBatis 프레임워크에서 JPA ORM을 도입하여 JPA 캐싱을 이용하여 성능을 보안하고자 JPA를 추가 도입하였습니다.
    • 도입 이유
      • 파일 기반MessageSource대비 Database 기반 MessageSource은 운영 시, 실시간으로 동적 수정이 가능하고 실시간 반영이 가능하며 Database를 통한 유지 보수가 편리한 반면 데이터베이스와의 통신 과정이 추가로 발생하기 때문에 데이터가 많아질 수록 성능이 비교적 떨어질 수 있습니다. 이것을 JPA의 데이터 2차 캐싱 기능을 활용하여 성능을 보완하기 위해서 도입하게 되었습니다.
  • 사용 예시
  1. Message Entity
@Entity
@Table(name = "messages")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
data class Message(

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(nullable = false)
    val messageKey: String,

    @Column(nullable = false)
    val locale: String,

    @Column(nullable = false)
    val message: String
      
)
  1. JpaRepository
interface MessageRepository : JpaRepository<Message, Long> {

    @Cacheable("messageCache")
    @Query("SELECT m FROM Message m WHERE m.messageKey = :key AND m.locale = :locale")
    fun findByMessageKeyAndLocale(
        @Param("key") key: String,
        @Param("locale") locale: String
    ): Optional<Message>
}
  1. MessageSource Component 생성
@Component
class DatabaseMessageSource(
    private val messageRepository: MessageRepository
) : AbstractMessageSource() {

    override fun resolveCode(code: String, locale: Locale): MessageFormat? {
        val message = messageRepository.findByMessageKeyAndLocale(code, locale.language)
            .map { it.message }
            .orElse(null)

        return message?.let { MessageFormat(it, locale) }
    }
}
  1. MessageConfigurer 생성
@Configuration
class MessageConfig(
    private val databaseMessageSource: DatabaseMessageSource
) {

    @Bean
    fun messageSource(): MessageSource {
        databaseMessageSource.setDefaultEncoding("UTF-8")
        databaseMessageSource.setFallbackToSystemLocale(false)
        databaseMessageSource.setCacheMillis(60000)
        return databaseMessageSource
    }
}

위와 같은 방법으로 Database MessageSource를 이용하고 추가적으로 Yaml Jcache 설정 및 ehcache.xml 등을 설정하여 2차 캐싱을 진행했습니다.

  • 성과
    • 파일 기반 MessageSource 사용 시 성능은 평균 15ms 으로 파일 기반으로 작동하기 때문에 빠른 속도가 나왔지만, JPA 2차 캐싱을 미 적용 하였을 때는 평균 200ms 으로 성능이 저하되었습니다. 하지만 2차 캐싱 적용 후 평균 24ms로 거의 유사한 속도가 나오게 되었습니다.
    • JPA 적용만 했을 때보다 2차 캐싱 적용으로 95% 정도의 성능 개선 효과가 있었습니다.
    • 또한 최초 호출로 DB에 접근, 이후 캐시 데이터를 활용하기 때문에 DB와의 통신 빈도 수가 줄어들게되어 네트워크 레이턴시가 줄어들게 되었습니다.
    • 데이터 베이스 리소스를 효율적으로 사용하여 운영 비용과 DB 성능 이슈를 최소화 할 수 있었습니다.
    • 운영팀에서 관리하여 유지보수 관리 비용이 크게 줄어들게 되었고, 협업 프로세스가 간소화되어 개발팀 업무의 생산성이 증가되었습니다.