오늘 배운 것

어제 Intellij에서 xml 파일을 작성해 주다가 xml에서 참조하는 dtd 파일의 링크가 유효하지 않아서 오류가 발생했었다. 그래서 우회하는 방법으로 여러 커맨드를 사용해 보니, 꼭 reveng.xml 파일을 사용하지 않아도 장고 모델을 스프링 엔티티로 바꿀 수 있는 것처럼 보였다. 

 

요약하자면 다음과 같다. 

1. 장고의 inspectdb 명령어를 이용해서 스키마 안의 테이블들을 전부 장고 모델로 바꾼다. 

2. hibernate에서 장고에서 사용하던 DB와 연결한다. 

3. hibernate tools를 통해 DB를 참고하여 스프링 엔티티 클래스를 만든다. 

4. 장고 모델과 동일하게, 1번을 참고하면서 4번의 스프링 엔티티 클래스를 수정한다. 

 

처음에는 1번 과정이 왜 필요한가 싶었는데, 혹시나 4번 과정에서 생성된 스프링 엔티티 클래스가 잘 생성된 것인지를 직접 비교하면서 판단하기 위해서는 필요할 것 같았다. 

 

1번 과정의 경우, 장고의 inspectdb라는 명령어를 사용하면 장고가 현재 사용하고 있는 DB(스키마)에 있는 모든 테이블들을 장고의 모델 클래스로 옮길 수 있다. 공식문서에도 친절하게 설명이 나와있었다.


그런데 inspectdb 명령어는 원래는 반대로 기존에 다른 프로젝트에서 사용하고 있는 데이터베이스를 장고 WAS에서도 참조하고 싶을 때 사용하는 것으로 보였다. inspectdb 명령어로 생성된 모델은 managed 속성값이 False로, 해당 모델의 CRUD(인스턴스가 아니라, 해당 모델 클래스에 대한 CRUD)가 장고 ORM에서 관리되지 않는다고 한다. 

 

그 외에도 데이터베이스별로 컬럼 타입 등이 조금씩 다를 수 있기 때문에 만약 데이터베이스의 어떤 컬럼 타입을 장고에서 사용하는 컬럼 타입으로 바꿀 수 없는 경우 기본으로 TextField를 할당한다고 되어있었다. 또한 데이터베이스에서 필드 이름으로 파이썬의 예약어(reserved word)를 사용한 경우 뒤에 '_field'를 추가로 붙여준다고 한다. 

 

inspectdb 커맨드의 소스 코드를 좀 보려고 했는데 양이 너무 길어서 다 보진 못했다. 'django.core.management.commands' 안에 있는 파일들이 모두 각각의 장고에서 제공하는 기본 커맨드이고, 모든 파일에서는 전부 BaseCommand라는 기본 클래스를 상속받는다. 

 

BaseCommand는 어떤 클래스도 상속받지 않은 기본 클래스이며, BaseCommand를 상속받은 여러 AppCommand, LabelCommand 등도 있다. AppCommand, LabelCommand 클래스들은 BaseCommand처럼 base.py에 정의된 걸 보니 얘네들을 상속받는 또 다른 커맨드 클래스들이 있는 것 같다. 

 

BaseCommand를 상속받아서 'django-admin'이나 'python manage.py' 뒤에 사용될 수 있는 커맨드를 만들고 싶다면 이 BaseCommand를 상속받으면 된다. 그리고 'handle'이라는 메소드를 오버라이딩해서 해당 커맨드가 어떤 역할을 할지를 정의해주면 된다. 

 

원리도 주석으로 상세히 설명되어 있었는데, 여러 메소드가 연쇄적으로 호출되는 것 같았다. 우선 처음에는 'django-admin'이나 'python manage.py' 명령어를 호출하면 뒤에 붙은 커맨드 클래스의 'run_from_argv' 메소드가 호출된다. 그리고 해당 메소드는 'create_parser' 메소드를 호출해서 해당 커맨드 뒤에 인자로 붙은 것이 있다면 'ArgumentParser' 클래스를 통해 파싱(parsing)을 한다. 

manage.py
django.core.management.__init__.py
django.core.management.__init__.py

그리고 그 파싱된 인자들을 가지고 'execute' 메소드를 실행한다. 'execute' 메소드에서는 'handle' 메소드를 호출하여 메소드의 핵심 로직을 실행시킨다. 만약 두 메소드 안에서 오류가 날 경우 'stderr'를 포함한 오류 메시지가 포함된다고 한다. 

django.core.management.base.py
django.core.management.base.py

 

아무튼 그래서 inspectdb도 당연히 handle 메소드가 있었다. handle 메소드 안에서는 handle_inspection이라는 또 다른 메소드를 호출하고 있었다. 

django.core.management.commands.inspectdb

handle_inspection 메소드는 너무 길어서 자세히 보진 못했다. 그래도 맨 처음에 DB에 커넥션을 맺고, 해당 커넥션을 통해 DB 스키마의 테이블들, 필드 이름들을 전부 가져오는 것으로 보였다. 아무튼 해당 작업을 통해서 DB 스키마에 있는 테이블들을 장고 모델로 바꿔준다는 것을 대략적으로나마 알 수 있었다. 

DB에 커넥션 맺기
DB 스키마의 테이블들 가져와서 for문으로 처리하기


이제 2번 작업으로 넘어가자. 그런데 아까처럼 .dtd 파일에서 오류가 나는 상황이다. 관련 공식문서를 찾아보았지만 현재 Reverse Engineering 기능은 유료인 Ultimate 버전에서만 제공되는 것 같았다. 알고보니 Intellij 무료 버전에는 내장된 데이터베이스 브라우저가 없기 때문에 이런 기능이 기본적으로 제공되지 않는다고 한다. 

 

사실 테이블 몇 개 옮기는 게 그렇게 번거로운 작업은 아닌데, 문제는 장고 모델이 변경될 때마다 계속 그에 맞춰 스프링 엔티티를 변경해 줘야 한다는 거였다. 그래서 JPABuddy라는 툴을 사용하는 방법도 있다고 한다. 무료 버전에서는 기능이 제한적인데, 최대한 무료 기능으로 한번 해결해 보자. 

 

궁금한 점

1. inspectdb 명령어로 생성된 장고 모델 중에서는 allauth나 simplejwt 같은 외부 라이브러리를 사용하면서 생성된 것들도 있는데, 이것들도 스프링 엔티티로 생성해야 할지 궁금하다. 왜냐하면 스프링에는 장고의 allauth, simplejwt와 똑같은 기능을 하는 라이브러리가 없을 수도 있고, 있더라도 그 라이브러리를 직접 사용해서 쓰는 게 더 맞기 때문이다. 이 부분은 잘 모르겠지만 일단은 이렇게 자체적으로 생성되지 않고 외부 라이브러리를 통해 자동적으로 생성된 모델들은 일단은 스프링 엔티티로 옮기지 말고 진행해야겠다. 

2. 'handle_inspection' 메소드에서 yield 문법이 나왔는데 여기에 대해서 잘 모르는 것 같다. 다음에 더 알아보자. 

 

 오늘 배운 것

이전 포스트를 올린 지 1주일이 훌쩍 넘어가고 있어서 이대로 가다간 사이드 프로젝트를 아예 못 이어나가겠다는 위기감이 있었다. 사실 이걸 안 하더라도 기존 프로젝트의 '하위 투두 프롬프팅' 관련 작업이 또 남아서 시간이 넉넉하진 않지만, 중간발표와 디버깅에 많이 순위가 밀려있던 이 녀석을 좀 챙겨줘야 하겠다는 생각이 들었다. 

 

오늘의 목표는 기존 Django WAS에서 사용하고 있는 RDS DB에 영향을 주지 않고 Spring Entity에 똑같은 정보를 가져오는 것이다. (사실 마이그레이션이라는 표현이 맞는지는 잘 모르겠어서 쓰다가 아니다 싶으면 제목을 고쳐봐야겠다. -> 마이그레이션이 맞는 듯 하다.)

 

나름 잘 설명했다고 생각했는데, 처음에는 녀석이 직접 변환하는 방법을 추천해줬다... 다행히 추가적인 질의로 좋은 방법을 알아낼 수 있었다. 

모델 개수가 많지는 않은데 귀찮다고 하기 좀 그래서 많다고 했다

 

Django model을 Spring entity로 변환하는 방법에는 여러 가지가 있었다. 결론적으로 나는 3번 방법을 선택했다가 리버스 엔지니어링의 장벽을 느끼면 1번으로 돌아오기로 했다. 

 

1. Django model을 SQL script로 변환한 뒤, SQL script를 Spring entity로 변환하기

2. JHipster 라는 툴을 사용하면 SQL 스키마를 Spring entity로 변환할 수 있다고 한다. 다 좋았는데, 변환 파일을 작성할 때 JDL이라는 JHipster 자체 문법을 사용해야 한다고 해서 그 점이 진입장벽이었다. 

3. Hibernate에서 리버스 엔지니어링을 사용하면 기존 DB 스키마에서 Spring entity를 자동으로 생성할 수 있다고 한다. 

4. Django model 파일을 parsing해서 직접 Java 클래스 파일로 변환하는 스크립트를 짜는 방법도 있는데 너무 번거롭고 굳이 다른 도구가 있는데 사용할 필요가 없다고 느꼈다. 

5. Liquibase나 Flyway같은 도구가 있었다. 다 너무 좋은데 유료여서 포기했다. 


그런데 잠깐이지만 또 궁금한 게 생겼다. 내가 Hibernate와 리버스 엔지니어링의 정확한 정의를 모르고 있었더라. 

 

Hibernate란?

공식문서를 참고해봤다. Hibernate는 Java에서 사용하는 ORM 프레임워크다. ORM이란 객체(object)와 관계형 데이터베이스(relational DB) 사이를 연결(mapping)해 주는 기법이다. 즉 ORM이 없던 시절에는 DB에서 조회한 값을 별도의 객체로 들고 있지 못했고, 그래서 그 값을 읽거나 쓰는 작업을 하려면 여러 가지를 조심해야 했고, 코드도 많이 작성해야 했다. 특히 두 가지가 불편했다. 첫째로는 SQL을 매번 작성해야 했고, 둘째로는 DB에 코드가 의존하게 된다는 점이었다. 각 DB마다 문법(dialect)이 조금씩은 다르기 때문이었다. 

 

아무튼, 이런 ORM 기법을 Java 언어에서 사용하도록 해 주는 ORM 프레임워크 중 하나가 Hibernate였고, 사실상 대부분의 개발에서 Hibernate를 많이 사용한다고 알고 있다. 

 

리버스 엔지니어링(Reverse Engineering)이란?

그렇다면 리버스 엔지니어링(reverse engineering)은 또 뭘까. 대강 역방향으로 코드를 분석한다는 정도로만 알고 있었는데 세부적인 정의는 좀 더 달랐다. 소스 코드를 보지 않고 컴파일된 프로그램의 동작 방식을 이해하는 것이 리버스 엔지니어링 이었다. 또한 보통은 ORM을 통해 객체 모델을 데이터베이스 스키마로 변환하는데, 이 반대의 작업(지금 내가 하려는 거)도 리버스 엔지니어링에 해당한다. 꼭 '어떤 작업'만 리버스 엔지니어링이 아니라, 보통 순방향으로 이뤄지는 엔지니어링을 역방향으로 진행하는 것이 리버스 엔지니어링에 해당한다고 이해했다. 


다시 돌아가 보자!

 

나는 Hibernate Tools를 사용해서 리버스 엔지니어링을 하려고 한다. 우선 리버스 엔지니어링을 하려면 hibernate를 build.gradle 파일에 dependency로 추가해 주고, 'ORM 매핑에 필요한 기본 정보를 포함한 파일'과 '리버스 엔지니어링에 필요한 파일'을 만들어 줘야 한다. 

 

우선 다음 내용을 build.gradle 파일에 추가해 준다.

dependencies {
	implementation 'org.hibernate:hibernate-core'
	implementation 'org.hibernate.tool:hibernate-tools'
}

 

그리고 'hibernate.cfg.xml' 이라는 파일을 만들어 준다. 이 파일에서는 ORM 매핑에 필요한 기본 정보가 포함된다. (민감 정보가 포함되어 있기도 하고, 어차피 .gitignore에 포함되어 로컬에서만 존재할 파일이라 ID와 PW 등은 가렸다.)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</property>
        <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/yourdatabase</property>
        <property name="hibernate.connection.username">yourusername</property>
        <property name="hibernate.connection.password">yourpassword</property>
        <property name="hibernate.hbm2ddl.auto">none</property>
        <property name="show_sql">true</property>
    </session-factory>
</hibernate-configuration>

 

또한 'reveng.xml' 파일도 만들어 준다. 이 파일에서는 리버스 엔지니어링에 필요한 기본 정보가 포함된다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-reverse-engineering PUBLIC "-//Hibernate/Hibernate Reverse Engineering DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-reverse-engineering-3.0.dtd">
<hibernate-reverse-engineering>
    <schema-selection match-catalog="yourdatabase"/>
    <table-filter match-name="*"/>
</hibernate-reverse-engineering>

 

그런데 코드 에디터(Intellij)에서 에러가 났다. 위의 두 XML 파일에서 참조하고 있는 .dtd 파일의 링크로 들어가 보니 해당 링크나 파일이 유효하지 않은 것 같았다. 찾아보니 DTD 파일이란 XML 문서에서 사용되는 구조와 규칙을 정의하는 문서라고 한다. 즉 XML 파일에서는 이 DTD 파일을 통해 어떤 규칙을 사용해야 할지 참고하려고 했는데 해당 링크가 유효하지 않아서 에러가 난 것으로 보였다. 

 

에러를 해결하고 마저 마이그레이션을 하고 싶었는데 밤이 늦었다... 일단 여기까지 세이브 해 두고 자야겠다!

 

 궁금한 점

1. 이 스프링 사이드 프로젝트를 진행하는 몇 가지 큰 이유 중 하나는 '똑같이 동작하는 WAS를 다른 프레임워크로 만들어 보면서 많이 배울 것 같아서'와 '스프링으로 만들면 취업에도 도움이 될 것 같아서' 였다. 그런데 막상 내가 '왜'를 놓치고 있다는 생각이 들었다. 단순히 스프링 쓰면 그걸로 끝일까? 물론 취업이나 현실적인 여건을 어느 정도 고려하는 건 당연하고 그럴 수는 있는데, 그냥 막연히 '스프링이니까!' 라고 쓰기엔 좀 2% 부족하다는 느낌이다... 왜 사람들은 스프링을 쓰는지에 대해서도 나만의 답을 내려보자. 

2. Hibernate의 구체적인 정의는 뭘까

3. 리버스 엔지니어링의 구체적인 정의는 뭘까

4. Django model이 바뀌면 Spring entity에 자동으로 반영해줄 수는 없을까? 역방향(Spring->Django)은 안 되고, 순방향(Django->Spring)으로만 반영되도록 자동화해주는 뭔가가 분명 있을 텐데 찾아봐야겠다. 

5. Hibernate와 JPA의 관계는? JPA는 또 뭐고 Hibernate랑은 어떤 관계가 있나?

 

 오늘 배운 것

오늘의 목표는 스프링 프로젝트에서 기존 장고 서버에서 사용하고 있던 RDS와 연결하는 것이다. 사실 스프링 프로젝트는 정말 오랜만이고 거의 처음과도 다르지 않아서 어떻게 시작해야 할지 감이 오지 않았다. 

모를 땐 GPT

기존에 스프링 프로젝트를 만들 때는 어떤 dependency가 필요할지 몰라서 최소한의 dependency인 Spring Web이랑 Lombok만 사용해서 만들었었다. 그런데 RDS랑 연결하기 위해선 DB Connection이 필요하고, 그러려면 Spring Data JPA와 MySQL Driver dependency가 추가로 필요했다(RDS가 MySQL로 되어있기 때문에 이게 필요하다). 

 

스프링 프로젝트에 대한 dependency를 추가하는 방법은 예전에 몇 번 해봐서 알고 있었다. mavenRepository 공식 사이트에 들어가서, 원하는 dependency를 검색한 다음 해당되는 코드 라인을 build.gradle 파일에 추가해 주면 되겠다. 

implementation 'org.springframework.data:spring-data-jpa'
implementation 'com.mysql:mysql-connector-j'

 

그리고 application.properties 파일과 관련된 설정도 해 줘야 하겠다. 해당 파일에서 DB와 연결하는 데 필요한 정보들을 정의해줄 수 있다. GPT 피셜, 다음과 같은 값들을 설정해 줘야 RDS에 연결이 가능하다고 한다. 

spring.datasource.url
spring.datasource.username
spring.datasource.password
spring.jpa.hibernate.ddl-auto
spring.jpa.show-sql
spring.jpa.properties.hibernate.dialect

 

spring.datasource.url은 말 그대로 연결하려는 DB의 엔드포인트이다. 다행히 서비스에서는 RDS를 사용하고 있고 퍼블릭 IPv4 엔드포인트도 정의되어 있으므로, 이 값을 그대로 가져와주면 되겠다. 

 

spring.datasource.username과 password는 DB에 접속하는 데 필요한 username과 password 값이다. 이 값은 장고 서버에서 정의하고 있는 DB_HOST와 DB_PASSWORD 값을 그대로 사용하면 되겠다. 

 

spring.jpa.hibernate.ddl-auto 값은 예전에 보았는데 생각이 잘 안 나서 문서를 찾아보았다. 기본적으로 스프링 JPA에서는 서버를 재시작할 때 'create-drop' 모드, 즉 매번 DB 테이블을 생성하고 다시 drop하는 것이 기본값으로 되어 있다고 한다. 지금 이 프로젝트는 사이드 프로젝트이며, 장고 서버나 RDS에 어떠한 영향도 주면 안 된다. 그러므로 이 값은 none으로 설정했다. 

 

값을 설정하는 것 자체는 문제가 없었는데, 문제는 이 파일을 그대로 깃허브에 올리면 안 된다는 점이었다. 그러면 application.properties 파일을 아예 올리지 말아야 할지, 아니면 해당 값들을 환경변수로 별도로 처리해야 할지 판단이 잘 서지 않았다. 

GPT는 두 방법 모두 가능하다고 말해주었는데, 내가 판단하기엔 아무리 혼자 개발한다고 해도 application.properties 파일을 아예 올리지 않는 것은 설정 파일을 아예 별도로 로컬에서만 관리하는 식이므로 너무 1인 개발에만 적합한, 확장이 어려운 방식이라는 생각이 들었다. 그래서 환경변수로 별도로 처리해주기로 했다. 

 

그러려면 java-dotenv라는 라이브러리가 별도로 필요했다. 정확히는 로컬에서 export 문으로 환경변수를 선언하면 해당 값을 별도의 라이브러리를 사용하지 않고도 application.properties 파일에서 사용할 수는 있었지만, 애초에 이 값은 프로젝트에서만 필요한 값인데 로컬에 별도로 정의하는 것은 맞지 않는다는 생각이 들었다. 

 

그래서 라이브러리를 어떻게 사용하면 좋을지를 또 물어보았다. 

 

GPT뿐만이 아니라 다른 블로그 글에서도 활용 방법을 세세하게 잘 알려주었다. 

implementation 'io.github.cdimascio:java-dotenv:5.2.2'

 

참고로 해당 라이브러리는 mvnRepository에서 검색해도 안 나오고, 깃허브 레포에서 나온 순수 서드파티 라이브러리인 듯 하다. 공식적으로 지원하는 기능이 아닌 것은 아쉽지만 서드파티이면 어떤가. 오히려 이렇게라도 기능이 있다는 게 감사하다. 제발 오류가 없기를..!

 

아무튼 build.gradle에 해당 라인을 넣어두고 수정사항을 잘 반영한 뒤, 추가로 해 줘야 할 작업이 있다. 

 

우선 src 디렉토리 바로 하위에 .env 파일을 추가하고, 불러오고 싶은 값들을 추가해 주자. 나의 경우는 다음과 같았다. 

 

그리고 이렇게만 하면 .env 파일에서 변수값을 자동으로 가져올 수는 없다고 해서 또 추가적인 작업이 필요하다고 한다. 

별도의 configuration 파일들을 모아두기 위해서 config 디렉토리를 만든 다음, 그 안에 DotenvConfig라는 Configuration 파일을 만들어 주었다. 이렇게 해야 .env 파일에 있는 환경변수 값들을 ${}으로 인식할 수 있는 것 같았다. 

import io.github.cdimascio.dotenv.Dotenv;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DotenvConfig {

    @Bean
    public Dotenv dotenv() {
        return Dotenv.load();
    }

}

 

필요한 설정은 다 되었겠거니 하고 서버를 시작해 보았는데 이런 오류가 났다. 아마도 .env 파일을 설정한 경로가 잘못된 것 같았다. 

 

역시 공식문서를 잘 읽어봐야 하겠다... GPT도, 블로그 글도 결국엔 공식문서를 따라잡을 수 없나보다. 

공식문서에 루트 디렉토리에 .env 파일을 생성하라고 딱 나와있었다. 바꿔서 생성하니 바로 서버가 떴다. 

 

궁금한 점 / 느낀 점 / 보완할 점

1. 프로젝트를 빌드할 때 build.gradle 파일이 구체적으로 어떤 역할을 하는지와 그 원리가 궁금하다. 소스 코드를 찾아보자

2. 스프링에서 application.properties 파일을 어떻게 참조하는지도 궁금하다. 문서가 참 방대해서 다 읽을 수는 없겠는데, 필요한 기능을 그때그때 찾아보고 적어도 내가 쓴 코드가 무슨 의미인지 알려고 해 보자

3. 나중에 spring-boot-dotenv 소스 코드도 한번 보고 어떻게 .env 파일에서 환경변수 값을 잘 가져오는지를 이해해 보자

4. spring.jpa.show-sql을 true로 하거나 false로 해야 하는 특별한 이유가 있을까? 단순히 어떤 SQL 쿼리가 실행되는지를 꼭 봐야 할 필요가 있는지 잘 이해되지 않아서 궁금하다

5. 기존에 잘 선언되어 있는 Django Model을 그대로 Spring Entity로 옮기고 싶다. 이걸 내가 소스 코드를 보면서 하나하나씩 바꾸는 방법도 있겠지만 과연 나와 비슷한 상황에 처한 사람이 한 명도 없었을까? 라는 생각이 든다. GPT와 티키타카를 좀 하면서 쉬운 방법이 있는지 찾아봐야겠다. 

 

+ Recent posts