ebson

[AI 활용과 개발] ChatGPT를 활용하여 JPA 개발하고 테스트하기 본문

KAKAO CHATBOT

[AI 활용과 개발] ChatGPT를 활용하여 JPA 개발하고 테스트하기

ebson 2024. 5. 10. 21:54

원하는 서비스의 요구사항에 맞추어 기술을 선택하고 이를 활용하기 위해 온라인 강의를 듣고 공식 개발 문서와 관련 개발 서적을 정독하는 방법도 좋겠지만 이미 학습을 마친 동료나 조수가 있다면 그에게 요구사항을 전달하여 템플릿 코드를 얻을 수 있을 것이다. 필자는 Spring Data JPA를 활용하기 위해 먼저 관련 온라인 강의를 수강했다. 그 결과 학습할 양이 많다는 사실을 알게 되었다. 그래서 내가 계획하고 있던 학습량을 수없이 끝내고도 남았을 ChatGPT 에게 도움을 요청하기로 했다. 

 

ChatGPT 에게 JPA 에 대한 질문하기

Spring Data JPA 를 사용하기 위한 기본 질문들을 ChatGPT에게 해보았다. 필수 의존성과 설정 코드, 핵심 원리, 영속성 컨텍스트의 장점을 질문해보았다. 아래는 ChatGPT 의 대답이다.

ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 1/6
ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 2/6
ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 3/6
ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 4/6
ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 5/6
ChatGPT 와의 대화 - JPA 에 대한 질문과 대답 6/6

 

참고로, 아래와 같이 질문하는 키워드가 명확한 경우에는 키워드만 전달하는 것에 ChatGPT가 더 구체적인 답변을 제공하는 모습이었다. 

 

ChatGPT 와의 대화 - JPA 기본에 대한 설명 (키워드만 제공)
ChatGPT 와의 대화 - JPA 기본에 대한 설명 (키워드만 제공)

 

이상의 내용을 참고하고 개인적으로 공부한 내용들을 적용해 아래와 같이 Spring Data JPA 를 사용하기 위한 코드를 application.properties 파일과 build.gradle.kts 파일에 추가했다. 

# JPA configuration
spring.jpa.hibernate.ddl-auto=none

spring.jpa.hibernate.naming.physical-strategy = org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

# spring.jpa.hibernate.ddl-auto=create
# spring.jpa.hibernate.ddl-auto=create-drop
# spring.jpa.hibernate.ddl-auto=update
# spring.jpa.hibernate.ddl-auto=validate

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

 

implementation("org.springframework.boot:spring-boot-starter-data-jpa")

 

 

ChatGPT 에게 테이블 생성문을 Prompt로 전달하여 Entity 클래스 작성 요청하기

기본 설정을 마쳤으므로 Spring Data JPA 의 영속성 컨텍스트를 활용하기 위한 Entity 클래스를 작성해야 한다. 이를 위해 데이터베이스에 생성한 테이블의 컬럼 하나하나를 엔티티 클래스의 필드로 옮겨 적을 수 있지만, 정답과 패턴이 있는 작업이었기 때문에 ChatGPT에게 맡겨보았다. 

 

ChatGPT 와의 대화 - 테이블 생성문을 전달하고 Entity 작성 요청하기 1/4

 

ChatGPT 와의 대화 - 테이블 생성문을 전달하고 Entity 작성 요청하기 2/4

 

ChatGPT 와의 대화 - 테이블 생성문을 전달하고 Entity 작성 요청하기 3/4

 

ChatGPT 와의 대화 - 테이블 생성문을 전달하고 Entity 작성 요청하기 4/4

 

 

테이블명과 컬럼명의 매핑, 데이터베이스의 자료형과 자바의 자료형 매핑, 연관관계 까지의 번거로운 작업들을 ChatGPT가 단 몇초만에

해주었다. 그리고 아래와 같이 추가 요구사항을 적용하는 것도 가능했다. 

 

 

ChatGPT 와의 대화 - Entity 클래스 추가 요구사항 적용 요청하기 1/3

 

ChatGPT 와의 대화 - Entity 클래스 추가 요구사항 적용 요청하기 2/3

 

ChatGPT 와의 대화 - Entity 클래스 추가 요구사항 적용 요청하기 3/3

 

 

다음으로 생성일시와 최근수정일시를 적용하기 위한 어노테이션의 동작방식에 대해서 추가로 질문해보았다. 

 

ChatGPT 와의 대화 - 생성일시와 최근수정일시 적용을 위한 어노테이션 설명 1/5

 

ChatGPT 와의 대화 - 생성일시와 최근수정일시 적용을 위한 어노테이션 설명 2/5

 

ChatGPT 와의 대화 - 생성일시와 최근수정일시 적용을 위한 어노테이션 설명 3/5

 

ChatGPT 와의 대화 - 생성일시와 최근수정일시 적용을 위한 어노테이션 설명 4/5

 

ChatGPT 와의 대화 - 생성일시와 최근수정일시 적용을 위한 어노테이션 설명 5/5

 

이상의 생성일시와 최근수정일시 적용을 위한 어노테이션 설명에 대한 ChatGPT의 대답을 정리하면 다음과 같다. @PrePersist 어노테이션은 엔티티가 데이터베이스에 저장되기 전에 실행되며 유효성 검사를 해야하는 경우에 사용할 수 있고 @CreationTumeStamp 어노테이션은 엔티티가 데이터베이스에 저장되는 시점에 현재 일시를 적용한다. 마찬가지로 @PreUpdate 어노테이션은 엔티티가 데이터베이스로 업데이트되기 전에 실행되며 검증 로직을 수행하는 경우에 사용할 수 있고 @UpdateTimestamp 어노테이션은 엔티티가 데이터베이스로 업데이트되는 시점에 현재 일시를 적용한다.

 

ChatGPT가 제공해준 내용들을 참고하여 MariaDB 으로 생성한 SKILL_RES_V1_VERSION 테이블에 대하여 다음과 같은 SkillResV1VersionEntity 클래스를 작성할 수 있다. 

 

# SKILL_RES_V1_VERSION 테이블 생성문

CREATE TABLE `SKILL_RES_V1_VERSION` (
	`VERSION_ID`	BINARY(16)	NOT NULL	DEFAULT UNHEX(REPLACE(UUID(),'-','')),
	`BLOCK_ID`	VARCHAR(45)	NOT NULL,
	`BLOCK_CODE` VARCHAR(45) NOT NULL, 
	`MAJOR`	INT	NULL,
	`MINOR`	INT	NULL,
	`CREATED_DATE`	DATETIME	NOT NULL	DEFAULT CURRENT_TIMESTAMP,
	`LAST_UPDATED_DATE`	DATETIME	NOT NULL	DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	`CREATOR`		VARCHAR(45)	NULL,
	`LAST_UPDATER`	VARCHAR(45)	NULL
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin 
;

ALTER TABLE `SKILL_RES_V1_VERSION` ADD CONSTRAINT `PK_SKILL_RES_V1_VERSION` PRIMARY KEY (
	`VERSION_ID`
);

ALTER TABLE `SKILL_RES_V1_VERSION` ADD CONSTRAINT `FK_VERSION_BLOCK_ID` FOREIGN KEY (
	`BLOCK_ID`
)
REFERENCES `BUILDER_V1_BLOCK` (
	`BLOCK_ID`
);

 

// SkillResV1VersionEntity 클래스

@Entity
@Table(name = "SKILL_RES_V1_VERSION")
@Getter @Setter
public class SkillResV1VersionEntity {

    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(name = "VERSION_ID", nullable = false, columnDefinition = "BINARY(16)")
    @Convert(converter = UUIDToBytesConverter.class)
    private UUID versionId;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "BLOCK_ID", referencedColumnName = "BLOCK_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_VERSION_BLOCK_ID"))
    private BuilderV1BlockEntity builderV1BlockEntity;

    @Column(name = "BLOCK_CODE", nullable = false, length = 45)
    private String blockCode;

    @Column(name = "MAJOR")
    private Integer major;

    @Column(name = "MINOR")
    private Integer minor;

    @Column(name = "CREATED_DATE", nullable = false)
    @CreationTimestamp
    private LocalDateTime createdDate;

    @Column(name = "LAST_UPDATED_DATE", nullable = false)
    @UpdateTimestamp
    private LocalDateTime lastUpdatedDate;

    @Column(name = "CREATOR", length = 45)
    private String creator;

    @Column(name = "LAST_UPDATER", length = 45)
    private String lastUpdater;

    @PrePersist
    protected void onCreate() {
        /**
         * 저장 전 검증 로직 수행
         * LocalDateTime now = LocalDateTime.now();
         * this.createdDate = now;
         * this.lastUpdatedDate = now;
         */
    }

    @PreUpdate
    protected void onUpdate() {
        /**
         * 업데이트 전 검증 로직 수행
         * this.lastUpdatedDate = LocalDateTime.now();
         * */
    }
}

 

 

BINARY(16) 형의 컬럼을 UUID 필드에 매핑하기 위한 컨버터 클래스 작성하기

한편, SKILL_RES_V1_VERSION 테이블의 VERSION_ID 컬럼의 자료형이 BIMARY(16) 인 이유는 UUID 문자열을 변환하여 저장하는 방식을 선택했기 때문이다. 그래서 엔티티 클래스에서 이를 UUID 자료형으로 저장하려면 컨버터 클래스가 필요하다. JPA 에서는 데이터베이스 컬럼의 자료형과 엔티티 클래스의 자료형 사이의 변환을 위해 AttributeConverter<X,Y> 인터페이스를 다음과 같이 제공한다. 

 

 

package jakarta.persistence;

/**
 * A class that implements this interface can be used to convert 
 * entity attribute state into database column representation 
 * and back again.
 * Note that the X and Y types may be the same Java type.
 *
 * @param <X>  the type of the entity attribute
 * @param <Y>  the type of the database column
 */
public interface AttributeConverter<X,Y> {

    /**
     * Converts the value stored in the entity attribute into the 
     * data representation to be stored in the database.
     *
     * @param attribute  the entity attribute value to be converted
     * @return  the converted data to be stored in the database 
     *          column
     */
    public Y convertToDatabaseColumn (X attribute);

    /**
     * Converts the data stored in the database column into the 
     * value to be stored in the entity attribute.
     * Note that it is the responsibility of the converter writer to
     * specify the correct <code>dbData</code> type for the corresponding 
     * column for use by the JDBC driver: i.e., persistence providers are 
     * not expected to do such type conversion.
     *
     * @param dbData  the data from the database column to be 
     *                converted
     * @return  the converted value to be stored in the entity 
     *          attribute
     */
    public X convertToEntityAttribute (Y dbData);
}

 

 

보는 바와 같이 X 자료형에서 Y 자료형으로, Y 자료형을 X 자료형으로 변환하는 메서드 2개를 가지는 간단한 인터페이스이다. 내가 변환하려는 자료형을 적용해 구현체를 작성하고 엔티티 클래스에 등록하면, JPA 에서 변환 작업을 해준다. UUID 자료형과 byte[] 자료형 사이의 변환을 위한 UUIDToBytesConverter 클래스를 다음과 같이 작성할 수 있다. 

 

@Converter(autoApply = true) // 이 컨버터를 자동으로 적용하도록 설정합니다.
public class UUIDToBytesConverter implements AttributeConverter<UUID, byte[]> {

    @Override
    // UUID 객체를 데이터베이스 컬럼에 저장하기 위해 byte[]로 변환합니다.
    public byte[] convertToDatabaseColumn(UUID uuid) {
        if (uuid == null) {
            return null; 
        }
        long mostSigBits = uuid.getMostSignificantBits();
        long leastSigBits = uuid.getLeastSignificantBits();
        return ByteBuffer.allocate(16) 
                .putLong(mostSigBits) 
                .putLong(leastSigBits) 
                .array(); 
    }

    @Override
    // 데이터베이스의 byte[]를 애플리케이션에서 사용하기 위해 UUID 객체로 변환합니다.
    public UUID convertToEntityAttribute(byte[] bytes) {
        if (bytes == null || bytes.length != 16) {
            return null; 
        }
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 
        long mostSigBits = byteBuffer.getLong();
        long leastSigBits = byteBuffer.getLong();
        return new UUID(mostSigBits, leastSigBits);
    }
}

 

JPA 활용 코드 완성하고 테스트 하기

데이터베이스 테이블들을 JPA 를 통해 관리하기 위해서 Entity 클래스를 작성한 후에는 JpaRepository 를 extends 하여 인터페이스에서 제공해주는 메서드를 활용할 수 있다. 아래와 같이 작성하고 테스트해보았다.

 

@Repository
public interface SkillResV1VersionEntityRepository extends JpaRepository<SkillResV1VersionEntity, UUID> {
}

 

 

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class SkillResV1VersionEntityRepositoryTest {
    
    @Autowired
    private SkillResV1VersionEntityRepository repository;
    
    ... 생략

    @Test
    public void getReferenceByIdTest(){
        UUID versionId = UUID.fromString(UUIDFormatter.formatToUUID("EC26D695038D11EFB5380A48BC1A5EE1"));
        SkillResV1VersionEntity entity = repository.getReferenceById(versionId);

        Assertions.assertNotNull(entity);
        Assertions.assertEquals(entity.getBlockCode(), "FP_S01_B01");
    }

    ... 생략

}

 

 

 

 

ChatGPT 를 활용한 JPA 개발 후기 

이상과 같이 ChatGPT 를 활용해 JPA 으로 데이터베이스 테이블들을 관리하기 위한 코드를 작성하고 테스트 해보았다. 테이블 컬럼을 JPA Entity 클래스의 필드로 옮기는 작업 중 번거로운 작업을 ChatGPT 가 보다 빠르고 정확하게 대신 작업해준다는 것 외에도 궁금한 점이 있으면 키워드 만으로도 실시간으로 정제된 대답을 얻을 수 있는 점이 개발과정의 비효율성을 개선함과 동시에 집중도 향상에도 도움이 되는 것 같다.

 

시중에 정형화된 패턴이 있는 코드들의 자동 생성을 돕는 솔루션들이 많은 것으로 아는데, 간단한 내용이라면 이상과 같이 ChatGPT에게 적절한 Prompt 를 입력해주는 것만으로도 충분히 도움을 받을 수 있을 것 같다. 필자는 이상과 같은 방법으로 퇴근 후 적은 시간을 투자했음에도 불구하고 20개 이상의 엔티티 클래스와 레포지터리 인터페이스 작성 및 테스트 작업을 단 며칠만에 마무리할 수 있었다. 

Comments