유미의 기록들

[개인과제 -Spring 숙련] 회원 CRUD API 구현 (Level 2) 본문

대외활동 기록/내일배움캠프

[개인과제 -Spring 숙련] 회원 CRUD API 구현 (Level 2)

지유미 2024. 8. 29. 17:01
728x90
반응형
JPA를 활용하여 회원 CRUD API 구현하기
- 유저 저장, 단건 조회, 전체 조회, 삭제 기능
- 유저는 유저명, 이메일, 작성일, 수정일 필드를 가지고 있음

 

📝  개발 과정

1. schedules 데이터 베이스 생성

create database schedules;
use schedules;

2. JPA 환경 설정하기

 build.gradle파일의 dependencies에 JPA 라이브러리를 설치한다

// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

3. 연결 정보 설정

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/schedules
spring.datasource.username={연결 계정명}
spring.datasource.password={비밀번호}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

 

application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  datasource:
    url: jdbc:mysql://localhost:3306/schedules
    username: {연결 계정명}
    password: {비밀번호}
    driver-class-name: com.mysql.cj.jdbc.Driver

프로그램을 실행했을 때 다음과 같은 로그가 나오면 연결 성공 !

먼저 회원 테이블을 생성하기 위해 ERD를 참고하여 Entity를 생성하였다

 

JPA에서 중요한 부분은 객체와 테이블을 매핑하는 것이다. Member객체와 테이블을 매핑하려고 한다

4. Entity 생성

@Entity
@Getter
@Setter
@Table(name="member")
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name="user_name", nullable = false, length = 15)
    private String userName;

    @Column(length = 30)
    private String email;

    @Column(name="write_date", nullable = false)
    private String writeDate;

    @Column(name="update_date")
    private String updateDate;

    protected Member(){

    }
    public static Member createNewMember(String userName,String email,String writeDate){
        Member newMember=new Member();
        newMember.userName=userName;
        newMember.email=email;
        newMember.writeDate=writeDate;
        return newMember;
    }
}

`@Entity` : JPA가 사용하는 객체로 JPA에서 해당 클래스를 데이터베이스 테이블로 관리하는 엔티티로 인식한다

`@Id` : 고유한 식별값인 PK(Primary Key)와 해당 필드를 매핑한다

`@GerneratedValue(strategy = GenerationType.IDENTITY)` : PK 생성 값을 MySQL에서 auto increment 역할을 한다

`@Column` : 테이블의 컬럼과 객체의 필드를 매핑한다 

- `name="user_name"` : 테이블의 컬럼명을 결정한다. (객체 필드의 카멜케이스를 테이블의 컬럼 _로 자동으로 변환해주기 때문에 이와 같은 경우에는 생략이 가능하다)

- `nullable = false` : NULL을 허용하지 않는다

- `length = 15`: 테이블의 컬럼 길이 값을 결정한다 (= VARCHAR 15)

JPA는 public또는 protected의 기본 생성자가 필수이다

protected Member(){}

외부에서 생성 부분의 코드를 통일시키기 위해 protected 접근제어자로 지정하였고, createNewMember함수를 통해 생성하도록 하였다

 

5. Controller

API명세서에 따라 클라이언트의 요청을 받아서 해당 메소드가 필요한 로직을 수행하도록 구현하였다

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
public class MemberController {
    private final MemberService memberService;
    //회원 저장
    @PostMapping()
    public CreateMemberResponse createMember(@RequestBody CreateMemberRequest request){
        return memberService.createMember(request);
    }
    //회원 전체 조회
    @GetMapping()
    public MemberListResponse getMemberList(){
        return memberService.getMemberList();
    }
    //회원 단건 조회
    @GetMapping("/{id}")
    public MemberResponse getMemberById(@PathVariable("id") Long id){
        return memberService.getMemberById(id);
    }
    //회원 수정
    @PutMapping("/{id}")
    public UpdateMemberResponse updateMember(@PathVariable("id") Long id, @RequestBody UpdateMemberRequest request){
        return memberService.updateMember(id,request);
    }
    //회원 삭제
    @DeleteMapping("/{id}")
    public DeleteMemberResponse deleteMember(@PathVariable("id") Long id){
        return memberService.deleteMember(id);
    }
}

CreateMemberRequest, CreateMemberResponse와 같은 DTO를 사용하여 클라이언트로 부터 요청을 받고 Service로 넘겨주도록 하였다 이렇게 하면 Entity와 API스펙을 명확히 분리할 수 있으며, 엔티티가 변해도 API스펙이 변하지 않게 된다

 

6. Service

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    public CreateMemberResponse createMember(CreateMemberRequest data){
         //회원 객체 생성
        Member newMember=Member.createNewMember(
                data.getUserName(),
                data.getEmail(),
                data.getWriteDate()
        );
        //저장
        Member savedMember=memberRepository.save(newMember);

        return new CreateMemberResponse(
                "회원 저장 성공",
                201,
                savedMember.getId()
        );
    }
    public MemberListResponse getMemberList(){
        //전체 조회
        List<Member> foundMemberList=memberRepository.findAll();

        //객체 -> DTO
        List<MemberDto> memberDtoList=foundMemberList.stream()
                .map(member -> new MemberDto(
                        member.getId(),
                        member.getUserName(),
                        member.getEmail(),
                        member.getWriteDate(),
                        member.getUpdateDate()
                )).toList();

        return new MemberListResponse(
                "회원 전체 조회 성공",
                200,
                memberDtoList.size(),
                memberDtoList
        );
    }
    public MemberResponse getMemberById(Long id){
        //단건 조회
        Member foundMember=memberRepository.findById(id);
        MemberDto memberDto=new MemberDto(
                foundMember.getId(),
                foundMember.getUserName(),
                foundMember.getEmail(),
                foundMember.getWriteDate(),
                foundMember.getUpdateDate()
        );
        return new MemberResponse(
                "회원 단건 조회 성공",
                200,
                memberDto
        );
    }
    @Transactional
    public UpdateMemberResponse updateMember(Long id,UpdateMemberRequest data){
        //회원 조회
        Member member=memberRepository.findOne(id);

        log.info("member.getUserName={}",member.getUserName());
        if(data.getUserName()!=null){
            member.setUserName(data.getUserName());
        }
        if(data.getEmail()!=null){
            member.setEmail(data.getEmail());
        }
        member.setUpdateDate(data.getUpdateDate());

        Member updatedMember=memberRepository.findOne(id);
        log.info("updatedMember.getUserName={}",updatedMember.getUserName());
        return new UpdateMemberResponse(
                "회원 수정 성공",
                200,
                updatedMember.getId()
        );
    }
    @Transactional
    public DeleteMemberResponse deleteMember(Long id){
        //회원 조회
        Member member=memberRepository.findOne(id);
        if(member!=null){
            memberRepository.deleteById(member);
        }
        return new DeleteMemberResponse(
                "회원 삭제 성공",
                200,
                id
        );
    }
}

API명세서에 따라 응답값을 별도의 DTO를 사용하여 반환하도록 하였다

반환값으로는 message, statusCode, 데이터 값을 ResponseDTO에 담는다

 

7. Repository

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager entityManager;
    //회원저장
    @Transactional
    public Member save(Member member){
        entityManager.persist(member);
        return member;
    }
    //회원 목록 조회
    public List<Member> findAll(){
        String jpql="SELECT m FROM Member m";
        return entityManager
                .createQuery(jpql,Member.class)
                .getResultList();
    }
    //회원 단건 조회
    public Member findById(Long id){
        String jpql="SELECT m from Member m where m.id=:id";
        return entityManager
                .createQuery(jpql,Member.class)
                .setParameter("id",id)
                .getSingleResult();
    }
    public Member findOne(Long id){
        return entityManager.find(Member.class,id);
    }
    public void deleteById(Member member){
        entityManager.remove(member);
    }
}

`private final EntityManager entityManager` : 스프링을 통해 `EntityManager`를 생성자 주입받는다. JPA는 기본적으로 한 요청 당, 하나의 EntityManager를 사용한다

`@Transcational` : JPA의 모든 데이터 변경(등록, 수정, 삭제)는 트랜잭션 안에서 수행되어야 한다.

 

저장

`entityManager.persist(member)` : JPA에서 객체를 테이블에 저장할 때는 `persist()`메소드를 사용한다

 //JPA가 만들어서 실행한 SQL
 insert into member (email, update_date, user_name, write_date) values (?, ?, ?, ?)

수정

`member.setUpdateDate(data.getUpdateDate())` : `entityManager.update()` 같은 메서드를 호출하지 않고, 엔티티 객체만 변경해도 UPDATE SQL이 실행된다.

*JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는 지 확인하고, 변경된 경우에는 UPDATE SQL을 실행한다. 이는 영속성 컨텍스트라는 JPA 내부원리를 이해해야 한다

//JPA가 만들어서 실행한 SQL
update member set email=?, update_date=?, user_name=?, write_date=? where id=?

조회

`entityManager.find(Member.class,id)` : 조회타입과 PK값을 주면 JPA에서 엔티티 객체를 PK 기준으로 조회한다. JPA가 조회SQL을 만들어서 실행하고, 결과값을 객체로 변환해서 준다

//JPA가 만들어서 실행한 SQL
select m1_0.id, m1_0.email, m1_0.update_date, m1_0.user_name, m1_0.write_date from member m1_0

JPQL (Java Persistence Query Language)

주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다. JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 만들게 된다

`entityManager.createQuery(jpql,Member.class)` : Member 엔티티 객체를 대상으로 "SELECT m FROM Member m"이라는 sql을 실행한다

* 여기서 동적쿼리를 생성하는데 큰 번거로움이 있는데, `Querydsl` 기술을 활용하면 깔끔하게 사용할 수 있다

 

💻 결과

회원 등록

 

회원 전체 조회

 

회원수정

 

회원 단건 조회

728x90
반응형
Comments