본문 바로가기

프로그래밍/Spring

[REST API 실습] 3. JPA(Hibernate) + HikariCP로 스프링부트 프로젝트와 RDS MariaDB 연동 후 CRUD 메소드 구현

본 포스팅은 스터디 모임의 발표 참고자료로 사용하기 위해 작성되었습니다.

 

 

목차

1) Springboot 프로젝트 생성하고 RestController 작성 후 실행하기(Gradle)

2) AWS RDS로 MariaDB 생성해서 워크벤치에 연결하기

3) JPA(Hibernate) + HikariCP로 스프링부트 프로젝트와 RDS MariaDB 연동 후 CRUD 메소드 구현

4) Springboot 프로젝트 AWS EC2 인스턴스에 배포

5) 안드로이드 앱에서 Retrofit을 사용해서 REST API와 통신하기(CRUD 구현)

 

참고사항

1) 부가적인 설명은 최대한 배제하는 대신 모든 과정을 여과 없이 스크린샷으로 남겼습니다. 그래서 대부분의 스크린샷이 창 전체를 포함합니다.

2) 이론적인 이해가 필요한 부분은 해시 태그(#)를 통해 키워드만 남겨놓도록 하겠습니다. 

3) 모든 과정을 정확하게 따르기 위해서는 IntelliJ, Gradle, Putty, AWS 계정, Mysql Workbench, Postman이 필요합니다.(테스트 과정을 패스한다면 Mysql Workbench, Postman은 필요 없습니다.)

 

 

 

3. JPA(Hibernate) + HikariCP로 스프링부트 프로젝트와 RDS MariaDB 연동 후 CRUD 메소드 구현

- 코드를 직접 입력하실 경우 오탈자에 유의해주세요. 약간만 다르게 입력되더라도 설정 문제로 인해 부팅이 불가능할 수 있습니다.

 

1) 라이브러리 의존성 추가

- 기존 의존성 아래에 라이브러리 의존성 3개를 추가합니다.

spring-boot-starter-data-jpa / mariadb-java-client / HikariCP

#JPA #Hibernate #HikariCP #ConnectionPool

 

- 코드

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // DB 연동
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.4.0'
    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.3.0'

}

 

2) 설정 파일 작성

- 두 가지 설정 파일을 작성합니다.

- 첫 번째 설정 파일은 application.yml 파일입니다.

- 들여쓰기에 따라 계층 구조가 생기기 때문에 계층 혹은 키가 잘못 입력되면 설정이 적용되지 않을 수 있습니다.

- 아래 코드에서 파란색으로 긁어진 부분에 RDS 데이터베이스 RDS를 입력하고 우측에 wickies에 스키마(사용자이름)을 입력합니다.

- 아래 username과 password를 데이터베이스 설정시 지정한대로 입력합니다.

- 쿼리 요청시 생성되는 쿼리를 확인하고 싶으면 hibernate 하위(format_sql과 같은 계층)에 show_sql: true의 주석(#)을 제거해주시면 됩니다. 

- 코드

server:
  port: 3333

spring:
  data:
    maria:
      jdbc-url: jdbc:mariadb://rds-mariadb.ch4ipcxitqs1.ap-northeast-2.rds.amazonaws.com:3306/wickies?useUnicode=yes&characterEncoding=UTF-8
      driver-class-name: org.mariadb.jdbc.Driver
      username: wickies
      password: qwer1234
  jpa:
    properties:
      hibernate:
        #show_sql: true
        format_sql: true
        temp:
          use_jdbc_metadata_defaults: false
    generate-ddl: true

 

- configuration 패키지를 작성하고 내부에 DBConfig 파일을 생성합니다.

- 어노테이션을 통해 스캔하기 때문에 파일명은 다르게 해도 무관합니다.

- 단 코드 안에 빈 이름은 연계되도록 지정되어있으니 오탈자에 유의해주세요.

- 아래 코드에서 TODO가 지정된 부분을 올바른 패키지명으로 변경해주세요.

#Bean #Datafactory #TransactionManager #EntityManagerFactory

- 코드

- import로 인한 에러를 피하기 위해 참조 사항에 대해 전부 첨부했습니다.

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "wlh.wickies.restapi.repository", // TODO Repository 패키지 지정
        transactionManagerRef = "mariaDB_transactionManager",
        entityManagerFactoryRef = "mariaDB_entityManagerFactory"
)
public class DBConfig {
    @Primary
    @Bean(name = "maria_dataSource")
    @ConfigurationProperties("spring.data.maria")
    public DataSource mariaDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Primary
    @Bean(name = "mariaDB_entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder,
            @Qualifier("maria_dataSource") DataSource dataSource) {
        Map<String, String> map = new HashMap<>();
        map.put("hibernate.ejb.naming_strategy", "org.hibernate.cfg.ImprovedNamingStrategy");
        map.put("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect");
        return builder.dataSource(dataSource)
                .packages("wlh.wickies.restapi.model") // TODO Model 패키지 지정
                .properties(map)
                .build();
    }

    @Primary
    @Bean(name = "mariaDB_transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("mariaDB_entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

}

 

3) 모델 클래스 수정 및 Repository 인터페이스 작성

- 기존에 생성했던 Member 객체에 몇몇 어노테이션을 추가해서 테이블 정의를 합니다.

- 많은 어노테이션이 추가되었습니다. 어떤 역할을 하는지 잘 모르시는 분은 반드시 검색해보시길 바랍니다.

- 코드

- 위 스크린샷에는 CreatedAt로 되어있지만 오타입니다. createdAt로 설정해주세요.

- 어노테이션을 통해 생성자를 만들었지만 실습때 사용할 생성자를 추가했습니다.(id와 createdAt 값을 입력할 필요가 없기 때문에 별도의 생성자가 필요합니다.)

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id")
    private long id;
    private String name;
    private int age;
    private String address;
    @CreationTimestamp
    private Date createdAt;

    public Member(String name, int age, String address){
        this.name = name;
        this.age = age;
        this.address = address;
    }
}

 

- MemberRepository 인터페이스를 추가합니다.

- 인터페이스는 비어있지만 JpaRepository를 상속받기 때문에 실습에 필요한 기본 메소드는 전부 사용 가능합니다. 필요한 메소드가 없을 때는 규칙에 맞게 메소드를 정의해서 사용하셔야 합니다.

#JpaRepository

- 코드

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> { }

 

4) Insert 테스트

- 데이터베이스가 비어있으니 Insert부터 테스트합니다.

- 아래 스크린샷에는 나와있지 않지만 컨트롤러에서 MemberRepository 인터페이스에 대한 의존성을 주입한 후에 사용할 수 있습니다.

-간단하게 만들기 위해 컨트롤러에서 직접 Repository를 호출했지만 Service 클래스를 따로 작성하는 패턴을 주로 사용하는 것 같습니다.

- Repository의 save 메소드로 Insert를 할 수 있습니다.

- 코드

@RequestMapping("/member")
@RestController
public class MemberController {

    @Autowired
    private MemberRepository memberRepository;
    
    @PostMapping("/insert") // CREATE
    public Member insert(){
        return memberRepository.save(
                new Member("Wickies", 20, "제주")
        );
    }
}

 

- 에러가 발생했습니다. address 칼럼에서 발생했으니 아마도 한글 인코딩 문제인것 같습니다.

 

5) 데이터베이스 인코딩 설정

- 스키마의 인코딩을 확인하니 역시나 latin1로 되어있습니다.

- 같은 증상으로 굉장히 고생한 경험이 있습니다. 데이터베이스에 정의된 인코딩들이 그렇게 많은지 몰랐습니다. 일일이 찾아서 수정했는데도 문제는 전혀 해결되질 않았었죠.

- 아무튼 고생한 결과 아주 간단한 해답을 찾았었습니다. 

#인코딩

 

- 스키마를 시원하게 드랍해버립니다.

 

- 그리고 다시 생성해줍니다.

 

- Charset을 utf8mb4로 지정합니다.

 

- 그리고 스프링 부트 프로젝트를 다시 실행합니다.

 

- 테이블이 정상적으로 다시 생성된 것을 확인할 수 있습니다.

 

- 다시 Insert를 시도하자 성공적인 결과를 반환합니다.

 

- 데이터베이스에도 정상적으로 입력이 되었습니다.

 

6) CRUD 코드 작성

- 프로젝트와 데이버테이스가 정상적으로 연동되어 작동하는걸 확인했으니 정식으로 CRUD 메소드를 작성합니다.

- Select를 제외하면 전부 Post 메소드를 사용합니다.

#GetMethod #PostMethod 

- 코드

- 위 스크린샷이랑 비교하면 약간은 달라진 코드가 있습니다. 

@RequestMapping("/member")
@RestController
public class MemberController {

    @Autowired
    private MemberRepository memberRepository;

//    @GetMapping("/test")
//    public Member memberTest(){
//        return new Member(0L, "Wickies", 20, "제주", new Date());
//    }
//
//    @GetMapping("/test2")
//    public ArrayList<Member> memberTest2(){
//        return new ArrayList(Arrays.asList(
//                new Member(1L, "Wickies", 20, "제주", new Date()),
//                new Member(2L, "마동석", 40, "전주", new Date()),
//                new Member(3L, "조승우", 22, "무주", new Date()),
//                new Member(4L, "박보영", 32, "진주", new Date()),
//                new Member(5L, "엄복동", 17, "상주", new Date())
//        ));
//    }

    @PostMapping("/insert") // CREATE
    public Member insert(@RequestBody Map<String, String> map){
        return memberRepository.save(
                new Member(map.get("name"), intParser(map.get("age")), map.get("address"))
        );
    }

    @GetMapping("/select") // READ
    public List<Member> selectAll(){
        return memberRepository.findAll();
    }

    @GetMapping("/select/{id}") // READ
    public Member selectOne(@PathVariable("id") long id){
        return memberRepository.findById(id).orElse(null);
    }

    @PostMapping("/update/{id}") // UPDATE
    public Member updateOne(@PathVariable("id") long id, @RequestBody Map<String, String> map){
        System.out.println(id);
        System.out.println(map);
        Member member = memberRepository.findById(id).orElse(null);
        member.setName(map.get("name"));
        member.setAge(intParser(map.get("age")));
        member.setAddress(map.get("address"));
        return memberRepository.save(member);
    }

    @PostMapping("/delete/{id}") // DELETE
    public String deleteOne(@PathVariable("id") long id){
        memberRepository.deleteById(id);
        return "삭제 완료";
    }

    int intParser(String age){
        try{
            return Integer.parseInt(age);
        } catch(ClassCastException e){
            e.printStackTrace();
            return 0;
        }
    }
}

 

 

7) POSTMAN을 사용해서 CRUD 테스트

- 요청을 보낼 수 있는 어플리케이션은 많습니다. 편하신대로 사용해도 좋습니다.

- 마동석이 삽입되었습니다.

 

- 마동석이 정상적으로 조회됩니다.

 

- 조승우를 추가했습니다.

 

- 마동석을 엄복동으로 수정합니다.

 

- 정상적으로 수정된 결과를 확인할 수 있습니다.

 

- 엄복동을 삭제합니다.

 

- 정상적으로 삭제된것을 확인할 수 있습니다.

 

 

 

간단한 REST API가 완성되었습니다.

 

4장에서는 AWS EC2를 이용해서 프로젝트를 배포합니다.