본문 바로가기
OpenSource/JPA

JPA 연동 가이드 - 개발편

by 태하팍 2024. 9. 10.
반응형

이런.. 개발편은 wiki에 작성했던것을 옮겨적으려고 했으나..gRPC 스터디 하면서 작성한거라
다시 작성 고고고~:)

회사 wiki 공유

JPA를 한다는건 DAO(Data Acess Obejct) or Repository단이라 DB랑 연관있는 친구라
gRPC를 하든 GraphQL을 하든 Restful로 하든 상관이 없습니다.
단지 객체를 가지고 테이블 관련 매핑하는 작업을 한다는 것 입니다!

이번에는 GraphQL때문에 GraphQL Query를 만들기 위한 Template이 필요합니다.
이 템플릿을 관리 할 수 있게 MySQL에 저장해서 사용하고자 합니다.

환경은 Spring Boot+Gradle+Jdk 22+MySQL

1) Gradle을 사용하고 있으니 build.gradle에서 Dependency를 걸어줍니다.
(아주 오래전엔..직접 찾아서 디렉토리 가서 넣어주곤했었죠ㅋㅋㅋㅋ)

첫줄 jpa만 필요 합니다~
MySQL을 사용하시면 있어야합니다.

2) application.yml에 아래와 같이 설정해줍니다.
     ddl-auto, show-sql 등을 설정하고 가장 중요한! 이론편에서 배운 Dialect을 설정 해줍니다.

spring:
  application:
    name: taehapark  
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect  # MySQL Dialect 설정
server:
  port: 8080

그리고 Database도 접속을 해야하니 접속 정보를 설정합니다.
username, url(jdbc:mysql로 시작, 맨뒤에 데이터베이스명), password, driver-class를 지정 합니다.

spring:    
  datasource:
    username: taeha
    url: jdbc:mysql://mysql.address.net:3306/(database명)
    password: 암호!
    driver-class-name: com.mysql.cj.jdbc.Driver

참고! Database가 접속이 가능해야 동작을 합니다. 접속이 불가하면 아래처럼 오류가 납니다.

그래서 사전에 얻은 정보를 가지고 IntelliJ의 경우 아래처럼 Database를 접속하게끔 해주는게 있습니다.

혹은 DBeaver 같은걸로 접속 테스트를 해봐도 좋습니다.

또한 DDL 형태를 확인 할 수 있습니다.

이제 JPA 연동을 위한 간단한 코드를 작성은 아래와 같습니다.

1. Entity 작성
   설계에 맞게 필요한 Entity를 만들어줍니다.
   요구사항은 Group과 Template의 관계는 1:N 입니다.

@Getter
@NoArgsConstructor
@Table(name = "graphql_groups")
@Entity
public class GraphQLGroup extends CommonTimeEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;  // template 명 (변경가능)

    @Column(name = "description")
    private String description;  // template 명 (변경가능)

    // Group 엔티티가 여러 개의 Template을 가질 수 있는 1:N 관계를 설정합니다.
    @OneToMany(mappedBy = "graphQLGroup", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<GraphQLTemplates> templates;

    @Builder
    public GraphQLGroup(String name, String description, List<GraphQLTemplates> templates) {
        this.name = name;
        this.description = description;
        this.templates = templates;
    }

    public void update(String name, String description, List<GraphQLTemplates> templates) {
        this.name = name;
        this.description = description;
        this.templates = templates;
    }

}
@Getter
@NoArgsConstructor
@Entity
@Table(name = "graphql_templates")
public class GraphQLTemplates extends CommonTimeEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;  // template 명 (변경가능)

    @Column(name = "description")
    private String description;  // template 명 (변경가능)

    @Column(name = "filter", length = 45)
    private String filter;  // template filter (GraphQL 컬럼들)

    @Column(columnDefinition = "TEXT", nullable = false)
    private String contents;  // template 내용 (GraphQL 컬럼들)

    // Template 엔티티가 Group 엔티티에 속하는 N:1 관계를 설정합니다.
    @ManyToOne
    @JoinColumn(name = "group_id", nullable = false)
    private GraphQLGroup graphQLGroup;

    @Builder
    public GraphQLTemplates(String name, String filter, String contents, String description, GraphQLGroup graphQLGroup) {
        this.name = name;
        this.filter = filter;
        this.contents = contents;
        this.description = description;
        this.graphQLGroup = graphQLGroup;
    }

    public void update(String name, String filter, String contents, String description, GraphQLGroup graphQLGroup) {
        this.name = name;
        this.filter = filter;
        this.contents = contents;
        this.description = description;
        this.graphQLGroup = graphQLGroup;
    }
}

2. controller 

@RestController
@RequiredArgsConstructor
public class GraphQLTemplateController {
    private final GraphQLTemplateService graphQLTemplateService;

    @PostMapping("/v1/save/template")
    public Long saveTemplateContent(@RequestBody TemplateRequestDto requestDto) {
        return graphQLTemplateService.crateTemplate(requestDto);
    }

    @GetMapping("/v1/groups/{groupName}/templates/{templateName}")
    public TemplateResponseDto getTemplateContent(@PathVariable("groupName") String groupName, @PathVariable("templateName") String templateName) {
        return graphQLTemplateService.findByGroupNameAndTemplateName(groupName, templateName);
    }
}

3. service 

@Service
@RequiredArgsConstructor
public class GraphQLTemplateService {
    private final GraphQLTemplatesRepository graphQLTemplatesRepository;
    private final GraphQLGroupRepository graphQLGroupRepository;

    @Transactional
    public Long crateTemplate(TemplateRequestDto requestDto) {
        // template은 group_id를 저장(GraphQLGroup 객체를 넘겨주면 됨)
        GraphQLGroup graphQLGroup = graphQLGroupRepository.findById(requestDto.getGroupId())
                .orElseThrow(() -> new RuntimeException("Group not found with id: " + requestDto.getGroupId()));
        return graphQLTemplatesRepository.save(requestDto.toEntity(graphQLGroup)).getId();
    }

    // 템플릿 조회
    public TemplateResponseDto findByGroupNameAndTemplateName(String groupName, String templateName) {
        Optional<GraphQLTemplates> entityOptional = graphQLTemplatesRepository.findByGroupNameAndTemplateName(groupName, templateName);
        if (entityOptional.isPresent()) {
            GraphQLTemplates entity = entityOptional.get();
            return new TemplateResponseDto(entity); // DTO 변환 로직
        } else {
            // 엔티티를 찾지 못했을 때의 처리 로직
            throw new EntityNotFoundException("Template not found");
        }
    }
}

4. Repositoty(조회)
    조회는 JPQL을 사용해서 가져옵니다.

public interface GraphQLTemplatesRepository extends JpaRepository<GraphQLTemplates, Long> {
    @Query("""
            SELECT DISTINCT gt
            FROM GraphQLTemplates gt
            JOIN gt.graphQLGroup gg
            WHERE gg.name = :groupName
            AND gt.name = :templateName
           """)
    Optional<GraphQLTemplates> findByGroupNameAndTemplateName(@Param("groupName") String groupName, @Param("templateName") String templateName);
}

5. Test Code
테스트코드는 처음에는 MySQL로 DDL로 테이블도 생성해보고 하다가 h2 in-memory db로 테스트하는 것으로 변경 하였습니다.
Controller Test로 @SpringBootTest로 진행을 하였는데 Repository Test는 @DataJpaTest를 가지고 JPA관련 컴포넌트만 로드하는 설정을 가지고 진행하면 됩니다.

@ExtendWith(MockitoExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-local.yml")
class GraphQLTemplateControllerTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private GraphQLTemplatesRepository graphQLTemplatesRepository;

    @After("Delete Data")
    public void tearDown() {
        graphQLTemplatesRepository.deleteAll();
    }

    @BeforeEach
    public void setUp() {
        createGroup("test_group", "h2 in-Memory db로 테스트 진행!");
    }

    // 전처리 작업을 위한 그룹 생성 메서드
    private Long createGroup(String name, String description) {
        GroupRequestDto groupRequestDto = GroupRequestDto.builder()
                .name(name)
                .description(description)
                .build();

        String groupUrl = "http://localhost:" + port + "/v1/save/group";
        ResponseEntity<Long> groupResponseEntity = restTemplate.postForEntity(groupUrl, groupRequestDto, Long.class);

        assertThat(groupResponseEntity.getStatusCode().is2xxSuccessful(), is(true));
        return groupResponseEntity.getBody();  // 그룹 생성 후 반환된 ID를 리턴
    }


    @Test
    public void Template_등록() {
        //given
        String templateName = "h2 in-Memory_test";
        String filter = "{test}";
        String description = "h2 in-Memory db로 테스트 진행!";
        String content = "mainImage {\n" +
                "    width\n" +
                "    height\n" +
                "    url\n" +
                "}";
        TemplateRequestDto requestDto = TemplateRequestDto.builder()
                .name(templateName)
                .description(description)
                .filter(filter)
                .contents(content)
                .groupoId(1L)
                .build();

        String url = "http://localhost:" + port + "/v1/save/template";

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

        // then
        assertThat(responseEntity.getStatusCode().is2xxSuccessful(), is(true));
        List<GraphQLTemplates> all = graphQLTemplatesRepository.findAll();
        assertThat(all, not(empty()));  // 리스트가 비어있지 않음을 검증
        assertThat(all.get(0).getName(), is(templateName));  // 그룹명 체크!
        assertThat(all.get(0).getFilter(), is(filter));
        assertThat(all.get(0).getDescription(), is(description));  // 그룹설명 체크!
        assertThat(all.get(0).getContents(), is(content));
    }
}

application-local.yml

logging:
  level:
    root: info
spring:
  output:
    ansi:
      enabled: always
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect # h2 dialect
        format_sql: true
  datasource:
    url: jdbc:h2:mem:test
    driverClassName: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true

팁) k8s deployment.yml에서 아래와 같이 env name으로 SPRING_PROFILES_ACTIVE을 설정해서 내부 Profile을 제어 합니다.

      containers:
        - image: hub/test-template:dev
          imagePullPolicy: Always
          name: test-template
          ports:
            - containerPort: 8080
              protocol: TCP
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: dev
반응형