Post

[프롬프트 엔지니어링] 개발

개발 진행

[프롬프트 엔지니어링] 개발

개발

  1. EducationContentController
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
     @GetMapping("/")
     @Transactional
     @Operation(
             summary = "get education Content",
             security = @SecurityRequirement(name = SECURITY_SCHEME_NAME),
             responses = {
                     @ApiResponse(
                             responseCode = "200",
                             description = "Successful operation",
                             content = @Content(
                                     mediaType = "application/json",
                                     schema = @Schema(implementation = SuccessResponse.class)
                             )
                     ),
                     @ApiResponse(
                             responseCode = "400",
                             description = "Bad request",
                             content = @Content(
                                     mediaType = MediaType.APPLICATION_JSON_VALUE,
                                     schema = @Schema(implementation = ErrorResponse.class)
                             )
                     ),
                     @ApiResponse(
                             responseCode = "401",
                             description = "Bad credentials",
                             content = @Content(
                                     mediaType = MediaType.APPLICATION_JSON_VALUE,
                                     schema = @Schema(implementation = ErrorResponse.class)
                             )
                     )
             }
     )
     public ResponseEntity<?> educationContent(@ModelAttribute GetEducationContentRequest request) throws JsonProcessingException {
         long id = Long.parseLong(request.getEducationContentId());
         EducationContentDTO dto = educationContentService.findById(id);
    
         if(dto == null){
             ErrorResponse errorResponse = ErrorResponse.builder()
                     .message("The requested content does not exist.")
                     .build();
             return ResponseEntity.badRequest().body(errorResponse);
         }
    
         ObjectMapper mapper = new ObjectMapper();
         mapper.registerModule(new JavaTimeModule());
         mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    
         return ResponseEntity.ok().body(mapper.writeValueAsString(dto));
     }
    
    1
    2
    
       - 교육용 컨텐츠의 id를 받아서, 해당되는 컨텐츠의 정보를 제공하는 메소드이다.
       - ```security = @SecurityRequirement(name = SECURITY_SCHEME_NAME)``` 을 추가하여, authentication이 존재할 때만 해당 메소드를 실행할 수 있도록 만들었다.
    


  1. CommandLineRunner

    CommandLineRunner는 Spring Boot에서 애플리케이션이 시작될 때 특정 로직을 실행할 수 있도록 해주는 인터페이스이다. 이 인터페이스는 단순히 run이라는 하나의 메서드를 가지고 있으며, 이 메서드는 애플리케이션이 완전히 시작된 후 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    @Override
    public void run(String... args) throws Exception {
        if (educationContentService.count() == 0) {
            log.info("Creating education content...");
            createEducationConetent();
            log.info("education content created.");
        }
    }
    
    private void createEducationConetent() {
        EducationContentDTO educationContentDTO = EducationContentDTO.builder()
                .contents("더미 컨텐츠")
                .title("Zero-Shot Prompting")
                .subject("Techniques")
                .createdAt(LocalDateTime.parse("2024-04-15T10:15:30"))
                .updatedAt(LocalDateTime.parse("2024-04-15T10:15:30"))
                .build();

        educationContentService.save(educationContentDTO);
    }
  • 애플리케이션이 실행된 다음에 Dummy 데이터를 DB에 저장한다.
  • 테스트 용으로 사용할 때 편리하다.


  1. Comment (댓글) 관련 클래스
    • Comment, CommentRepository, CommentService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Entity
@Getter
@Table(name = "comment")
public class Comment {
    @Id
    @Column(name = "comment_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonManagedReference
    Long commentId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    @JsonIgnore
    User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "education_content_id", referencedColumnName = "content_id")
    @JsonIgnore
    EducationContent educationContent;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="parent_comment_id", referencedColumnName = "comment_id")
    Comment parentComment;

    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
    @JsonBackReference
    List<Comment> childCommentsList = new ArrayList<>();

    @Column(name = "body")
    String body;

    @Column(name = "created_at")
    LocalDateTime createdAt;

    @Column(name = "modified_at")
    LocalDateTime modifiedAt;

    @Builder
    public Comment(Long commentId, User user, EducationContent educationContent, Comment parentComment, List<Comment> childCommentsList, String body, LocalDateTime createdAt, LocalDateTime modifiedAt) {
        this.commentId = commentId;
        this.user = user;
        this.educationContent = educationContent;
        this.parentComment = parentComment;
        this.childCommentsList = childCommentsList;
        this.body = body;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }

    public void addChildComment(Comment childComment){
        childCommentsList.add(childComment);
        childComment.parentComment = this;
    }

    public Comment() { }
}
  • users 테이블, education-content 테이블, comment 테이블(자기 자신)과 연관관계 매핑이 되어있다.
  • ParentComment는 자기 자신을 참조하는 필드이다. 부모 댓글을 의미한다.
  • childCommentsList는 자식 댓글(대댓글)의 리스트를 의미한다.


  1. 댓글 달기 기능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    @PostMapping("/{educationContentId}/new-comment")
    @Operation(
            summary = "post new comment to a specific Education Content",
            security = @SecurityRequirement(name = SECURITY_SCHEME_NAME),
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Successful operation",
                            content = @Content(
                                    mediaType = "application/json",
                                    schema = @Schema(implementation = SuccessResponse.class)
                            )
                    ),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Bad request",
                            content = @Content(
                                    mediaType = MediaType.APPLICATION_JSON_VALUE,
                                    schema = @Schema(implementation = ErrorResponse.class)
                            )
                    ),
                    @ApiResponse(
                            responseCode = "401",
                            description = "Bad credentials",
                            content = @Content(
                                    mediaType = MediaType.APPLICATION_JSON_VALUE,
                                    schema = @Schema(implementation = ErrorResponse.class)
                            )
                    )
            }
    )
    public ResponseEntity<CommentDTO> newComment(
            @PathVariable("educationContentId") String educationContentId,
            @RequestBody CommentRequest request ) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HH:mm:ss");

        CommentDTO dto = CommentDTO.builder()
                .educationContentId(Long.valueOf(educationContentId))
                .userId(UUID.fromString(request.getUserId()))
                .parentCommentId(Long.valueOf(request.getParentCommentId()))
                .body(request.getBody())
                .createdAt(LocalDateTime.parse(request.getCreatedAt(), formatter))
                .modifiedAt(LocalDateTime.parse(request.getModifiedAt(), formatter))
                .childCommentsIdList(null)
                .build();

        Long savedCommentId = commentService.save(dto);

        if(Long.valueOf(request.getParentCommentId()) != -1){
            // 대댓글안 경우, parent Comment에 자식 comment가 달렸음을 알려줘야 함.
            // 부모 Comment의 childCommentList에 새로운 노드 추가

            Long parentCommentId = dto.getParentCommentId(); // 기존에 존재하던 댓글 id
            Long childCommentId = savedCommentId; // 새로 추가된 대댓글 id

            commentService.updateChildCommentsList(parentCommentId, childCommentId);
        }

        dto.setCommentId(savedCommentId);
        return ResponseEntity.ok().body(dto);
    }
  • 일단 댓글에 관련한 정보를 사용자에게 받는다.
    • user id
    • 교육용 콘텐츠 id
    • 부모 댓글의 id
    • 내용
    • 생성시간, 수정시간
  • 댓글을 작성할 때, 자식 댓글은 없는 상태이므로 null 값을 삽입한다.

  • 만약, 부모 댓글이 존재하는 경우 (대댓글인 경우), 해당 부모 댓글의 childCommentsList에 새로운 자식을 추가해주어야 한다.


오류

1. PersistentBag

PersistentBag는 Hibernate에서 컬렉션을 관리하기 위해 사용하는 클래스 중 하나이다.

Hibernate는 JPA 구현체로서, 엔티티 간의 관계를 관리하기 위해 여러 가지 컬렉션 타입을 제공한다. PersistentBag은 그 중 하나로, 주로 java.util.List를 구현한다.

1
2
    @OneToMany(mappedBy = "educationContent")
    private ArrayList<Comment> commentList = new ArrayList<>();

ArrayList는 PersistentBag이 관리하지 않는다. 따라서, 다음과 같이 수정해야 한다.


<해결> ``` @OneToMany(mappedBy = "educationContent") private List commentList = new ArrayList<>(); ```
---
#### 2. @JoingColumn @JoinColumn(name = "id") 주의 ![](/assets/posts/a81966568c243b3abe0edd123d2c2e49fe48d12b060fa5902651f1d387cc0d9a.png) JoinColumn에서 외래키의 주인 쪽 컬럼 명과 매핑되는 쪽의 컬럼 명이 다르면 referencedColumnName 어노테이션을 사용해서 명시적으로 지정해야 된다.
---
#### 3. The given id must not be null 오류메시지: ```org.springframework.dao.InvalidDataAccessApiUsageException: The given id must not be null```
---
``` userRepository.findById(commentDTO.getUserId()).get() ``` ➜ ``` User user = userRepository.findById(commentDTO.getUserId()) .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); ``` 왜 그런건지 공부 필요
---
#### 4. LazyInitializationException >```org.hibernate.LazyInitializationException```는 영속성 컨텍스트가 종료되어 버려서, 지연 로딩을 할 수 없어서 발생하는 오류이다. <해결> ```@Transactional``` 을 컨트롤러의 메소드에 추가해주니 잘 실행되었다.
---
#### 5. mappingexception Docker 환경에서는 애플리케이션이 잘 돌아가는데, Local에서 돌리면 다음과 같은 오류가 발생했다. ``` org.springframework.data.mapping.mappingexception: parameter org.springframework.data.mapping.parameter@97b61336 does not have a name ```
<해결> 빌드 설정을 Intellij 에서 Gradle로 바꿔주니 잘 동작했다. 이러한 오류가 발생한 원인은 이 포스트에서 잘 설명해주셨다. >요약하자면, Spring 6.1 버전에서 ```LocalVariableTableParameterNameDiscoverer``` 라는 구현체가 삭제되었는데, Intellij에서 빌드할 때 사용하는 ```-g``` 옵션은 해당 구현체를 사용해서 파라미터의 이름을 가져온다. 따라서, Intellij에서 빌드하면 파라미터의 이름을 가져오지 못하는 MappingException이 발생했던 것이다.
---
#### 6. DTO -> JSON 순환참조 ``` Resolved [com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.hibernate.collection.spi.PersistentSet[0]->com.cluster23.promptfordev.entity.User$HibernateProxy$j1Hdqelu["terms"]->org.hibernate.collection.spi.PersistentBag[0]->com.cluster23.promptfordev.entity.Term["users"] ```
<해결> 웬만하면 양방향 매핑을 사용하지 말자. ```@JsonIgnore``` 어노테이션을 사용해서 직렬화할 때 특정 필드를 무시하도록 만든다.
``` @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", referencedColumnName = "id") @JsonIgnore User user; ```
- User를 무시하게 만들면, 직렬화할 때 user 필드는 빠진다. - 이 때, user_id 라는 빼고 Json을 작성한다는 것이 아니다! >이 설정을 통해, user 객체는 JSON에 포함되지 않으며 user_id 필드만이 포함된다. 여기서 user_id는 데이터베이스의 comment 테이블에 실제로 존재하는 값이고, JSON으로 변환될 때 포함된다. 그러나 User 객체 자체, 즉 user_id가 참조하는 User 테이블의 다른 컬럼들(예: role, term 등)은 JSON으로 변환되지 않는다. 이는 user_id는 포함하지만, User 테이블의 내용을 직렬화하지 않는다는 것을 의미한다.
---
### JPA와 관계형 DB JPA와 관계형 DB의 연관관계를 잘못 이해했다. 대댓글 서비스를 만들던 중, 부모 댓글에 자식 댓글 목록을 추가하는 로직을 작성했다. 근데, 아무리 추가를 해도 Comment 테이블의 child_comments_list 컬럼에는 아무 정보도 작성되지 않았다. 당연한거다. childCommentsList 라는 필드는 양방향 연관관계 매핑을 위한 필드이다. 실제 Comment 테이블에는 존재하지 않는다. DB와 객체를 너무 똑같은 선상에서 생각을 했던 것 같다.
#### 대댓글 관련 의문점 객체지향과 DB를 혼동하고 있는 사람이라면, 필자와 같은 의문을 갖게 될지도 모른다. 객체지향과 DB를 정확히 알고있는 사람이라면, 위의 의문이 왜 생기는지 조차 이해할 수 없을 것이다.
```java @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="parent_comment_id", referencedColumnName = "comment_id") Comment parentComment; @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) @JsonBackReference List childCommentsList = new ArrayList<>(); ``` > 왜 ```mappedBy = "parentComment"``` 이지? 필자는 ```mappedBy = "commentId"``` 가 되어야 한다고 생각했다. ```@OneToMany```에서 One은 현재 댓글을 의미하고, Many는 자식 댓글들을 의미한다고 생각했다. childCommentsList는 "현재 댓글"의 자식 댓글 리스트이지, "현재 댓글의 부모 댓글"의 자식 댓글 리스트가 아니다.
따라서, ```mappedBy = "commentId"``` 로 "현재 댓글"에 매핑되어야 한다고 생각했다. 근데 이것은 객체 지향적인 관점이다. DB의 관점에서 생각해보자. Team과 Member 테이블이 있을 때, Team A에 속한 member들을 조회하려면 어떻게 하는가? 모든 member 중 Team이 A인 것들을 찾는다. 위와 똑같이, 댓글 1번에 속한 대댓글들을 조회하려면 어떻게 해야 할까? 모든 Comment 중 ParentComment가 1인 것들을 찾는다. 따라서, ```mappedBy = "parentComment"```가 되어야 하는 것이다.
This post is licensed under CC BY 4.0 by the author.