개발/실습

Spring JPA를 활용한 간단한 게시판 만들기 - 회원 로직 구성

람무 2023. 5. 25. 18:22

지난번 기본설정에 이어 회원로직을 구성해보도록 하겠습니다.
회원만 글을 작성할 수 있는 게시판을 구성할 예정이라 회원가입 → 로그인 → 로그인에 성공하면 글 작성을 하는 패턴으로 진행 할 예정입니다. 다만 로그인 이후의 로직은 추후 보안로직을 완성한 후 적용할 예정이고 지금은 기본적인 회원가입 로직부터 구성하도록 하겠습니다.

 

Entity 구성

구분을 편하게 하기 위해서 member 패키지를 하나 만듭니다. 대부분의 작업 경로는

src → main → java → {파일명}

경로로 되어있고 {파일명}.{다른파일명} 으로 되어있거나 {파일명} 만으로 되어있을 수 있습니다. 가장 하위패키지에 패키지를 생성하신 후 Member 이름의 클래스를 만듭니다.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "MEMBER")
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long memberId;

    private String email;

    private String password;

    private String nickname;
}

 

Entity 클래스 입니다. 만약 JPA의 자동테이블 생성기능을 사용한다면 해당 클래스의 이름과 항목들이 그대로 테이블명과 컬럼이 됩니다. 테이블과 컬럼에 해당하는 값들을 넣어주는 기능을 한다고 볼 수 있습니다. 

@Getter/@Setter
@Getter는 외부 클래스에서 private 등으로 보호 된 값을 가져올 수 있게 해주는 편리한 어노테이션 입니다. 해당 어노테이션을 사용하지 않는다면 별도의 getter 값을 만들어줘야 합니다.

@Setter는 값을 수정할 수 있도록 해주는 어노테이션 입니다. 어노테이션 설정을 하지 않았다면 별도의 setter 값을 만들어줘야 합니다. Entity클래스에는 @Setter를 사용하지 않는 경우도 많습니다. 제가 @Setter를 사용하는 이유는 데이터 수정을 할 때 Setter값이 없으면 Entity클래스 내에 따로 update 메서드를 만들어주는 방식을 써야하는데 나중에 보여드릴 Service 클래스에서 구현할 update 로직과 상충된다고 생각하여 그렇습니다. Setter를 사용하지 않을 경우 update 메서드로 수정하고자 하는 영역의 생성자를 만들어주신 후 서비스클래스에서 해당 메서드를 가져오는 방법으로 처리하셔도 됩니다.

@AllArgsConstructor / @NoArgsConstructor
AllArgsConstructor은 매개변수가 있는 생성자를 만들어주고 @NoArgsConstructor은 매개변수가 없는 생성자를 만들어줍니다. 만약 @Build 패턴을 사용한다면 @AllArgsConstructor 대신 사용할 수 있습니다.

@Entity
해당 클래스가 Entity클래스임을 나타냅니다.

@Table
테이블 이름을 정할 수 있습니다. name 뒤에 테이블명을 설정합니다.

@Id
해당 테이블의 PK역할을 하는 항목입니다. GenerationType.IDENTITY는 데이터의 자동생성 전략을 설정해줍니다. 즉 회원가입을 진행하면 고유값은 자동으로 1부터 시작하여 숫자가 1씩 계속 증가한다고 볼 수 있습니다.

추가로 @Column 속성이 있는데 스키마에서 구성한 컬럼의 이름과 엔티티 클래스의 컬럼이름이 다를경우 붙여주면 됩니다. JPA 자동테이블 생성 기능을 사용할 경우 반드시 @Column을 붙여줘야 테이블의 컬럼값으로 지정이 됩니다. 컬럼명, 공백여부 등의 간단한 설정을 추가적으로 할 수 있습니다.

실질적으로 회원가입때 입력받는 부분은 email, password, nickname 입니다.

 

DTO 구성

@Getter
public class MemberDto {

    @AllArgsConstructor
    @Getter
    public static class Post {
        private String email;
        private String password;
        private String nickname;

    }

    @Getter
    @Setter
    public static class Patch {
    	private Long memberId;
        private String password;
        private String nickname;
    }

    @AllArgsConstructor
    @Getter
    public static class Response {
        private Long memberId;
        private String email;
        private String nickname;
    }
}

DTO클래스 입니다. 클라이언트의 요청을 Controller 클래스에서 처리를 하게 되는데 이 클래스에서 DTO 클래스를 통해 Service클래스를 거쳐서 Entity 클래스로 데이터가 이동하게 됩니다.

이런 느낌으로 볼 수 있습니다. DTO클래스를 생략하고 Controller클래스에서 바로 Service클래스로 데이터를 전달하는 로직을 만들수도 있습니다만 그렇게 되면 Controller 클래스의 로직이 복잡해질 수 있기 때문에 보통은 DTO 클래스를 분리해서 사용합니다. 그리고 Controller 클래스가 클라이언트의 요청을 받고 응답하는 기능 외에 Service클래스로 데이터를 전달하는 역할까지 하게되어서 코드 가독성도 떨어지게 될 수 있습니다.

저는 DTO클래스를 하나로 묶는게 편해서 저렇게 구성했지만 따로 나눠서 구성해도 무방합니다.
예를들어

@AllArgsConstructor
@Getter
public class MemberPostDto {
	private String email;
    private String password;
    private String nickname;
}
@Getter
@Setter
public class MemberPostDto {
    private String password;
    private String nickname;
}

이런식으로 DTO클래스를 따로 구분해서 만들어도 됩니다.

 

Repository 구성

public interface MemberRepository extends JpaRepository<Member, Long> {

}

Repository는 클래스가 아닌 인터페이스 형태로 만듭니다. Repository는 DB와의 통신을 추상화하고 데이터베이스 연산을 쉽게할 수 있는 메서드들을 제공합니다. 클래스가 아니라 인터페이스로 만드는 이유는 DB상호작용을 위한 추상화 계층 제공, 유연성과 확장성을 위해서 입니다.

쿼리를 자동생성하거나 @Query 어노테이션으로 직접 쿼리를 작성할 수 있고 특정 메서드를 따로 만들어서 구성할 수 있습니다.

 

Service 구성

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);
        return savedMember;
    }

    public Member updateMember(Member member) {
        Member findMember = identifyMember(member.getMemberId());

        Optional.ofNullable(member.getPassword()).ifPresent(password -> findMember.setPassword(password));
        Optional.ofNullable(member.getNickname()).ifPresent(nickname -> findMember.setNickname(nickname));

        return memberRepository.save(findMember);
    }

    public Member findMember (long memberId) {
        return identifyMember(memberId);
    }


    public void deleteMember (long memberId) {
        Member findMember = identifyMember(memberId);
        memberRepository.delete(findMember);
    }

    public Member identifyMember (long memberId) {
        Optional optionalMember = memberRepository.findById(memberId);
        Member findMember = optionalMember.orElseThrow(() -> new RuntimeException("회원이 없습니다"));
        return findMember;
    }
}

@Service 어노테이션을 사용하면 해당 클래스가 Service 클래스라는 것을 알릴 수 있습니다.
@RequireArgsConstructor은 final이 붙은 필드를 인자로 받는 생성자를 자동으로 생성해줍니다. 만약 해당 어노테이션을 사용하지 않았다면

이런식으로 생성자를 따로 만들어주시면 됩니다. 각 메서드들을 설명드리자면

createMember
회원을 생성하는 로직 입니다. DTO클래스를 통해 전달받은 데이터를 Repository 인터페이스를 활용하여 저장합니다.

updateMember
회원 정보를 수정하는 로직 입니다. identifyMember 는 회원을 조회하는 메서드로 회원 조회로직을 따로 구현하지 않고 별도의 메서드를 만든 이유는 해당 메서드를 만들어두면 조회, 삭제에도 사용할 수 있기 때문입니다.
회원정보는 비밀번호와 닉네임을 수정할 수 있게 구성했는데 Optional.ofNullable을 사용하게 되면 비밀번호 또는 닉네임 둘중 하나의 데이터만 보내주고 다른 하나를 Null로 전달해도 수정을 허용하게 하기 위함입니다. Null로 전달받은 값은 바뀌지 않고 따로 전달받은 데이터만 바뀌게 됩니다.

findMember
회원을 검색하는 로직 입니다. 추후 mypage 역할을 하게 될 것이며 전체 회원조회는 관리자 외에는 사용할 일이 없으므로 나중에 게시판 관련 기능을 구현할 때 전체조회 기능을 넣어두도록 할 예정입니다.

deleteMember
회원 삭제(탈퇴) 로직 입니다. 저장 된 회원을 삭제합니다. 개발 경우에 따라 삭제로직을 사용했을 때 DB에서 바로 지워버리는게 아니라 Entity 클래스에 Enum 타입으로 회원상태를 만들어서 상태값으로 관리하는 경우도 있습니다. 지금은 최대한 단순한 로직으로만 구성할 것이므로 해당 기능은 구현하지 않았습니다.

 

Mapper 구성

@Mapper(componentModel = "spring")
public interface MemberMapper {

    Member memberDtoToMember (MemberDto.Post post);
    Member memberPatchDtoToMember (MemberDto.Patch patch);
    MemberDto.Response memberToMemberResponseDto (Member member);

}

매퍼인터페이스는 Controller클래스에서 전달받은 데이터를 DTO를 거쳐서 Entity클래스로 전달해주는 역할을 합니다. 

매퍼인터페이스를 생략하고 Fasade 패턴을 사용하거나 DTO클래스, Service클래스 Entity클래스가 직접 데이터 전달을 주고받게 하는 경우도 있습니다. 저는 추후 게시판 영역과 회원 영역을 연관관계 매핑을 해줄 예정이라 매퍼인터페이스를 사용했고 연관관계 매핑을 하지 않는다면 Entity 클래스에 연관관계를 맻을 클래스의 PK값을 컬럼으로 만들어서 데이터를 불러오는 식으로 활용할 수도 있습니다.

매퍼 인터페이스를 보면 별다른 로직이 없는데요

인텔리제이 우측 상단의 Gradle클릭 → board → Tasks → build → build를 더블클릭하면 자동으로 매핑 로직을 작성해줍니다. 빌드가 완료되면

프로젝트 내 src 패키지 위쪽을 보시면 build 패키지가 있고 거기서 쭉 타고 내려가다보면 MemberMapperImpl 이라는 파일이 생성되어있습니다. 해당 파일을 통해서 로직이 구현되게 됩니다. 만약 직접 로직을 설정하실 경우엔 Mapper 인터페이스에서 직접 로직을 구현해주실 부분 앞에 default 를 붙이신 후 로직을 구현하시면 됩니다.

 

Controller 구성

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final MemberMapper memberMapper;

    @PostMapping("/signup")
    public ResponseEntity postMember (@RequestBody MemberDto.Post post) {
        Member registerMember = memberService.createMember(memberMapper.memberDtoToMember(post));
        return new ResponseEntity(memberMapper.memberToMemberResponseDto(registerMember), HttpStatus.CREATED);
    }

    @PatchMapping("{member-id}")
    public ResponseEntity patchMember (@PathVariable ("member-id") long memberId,
                                       @RequestBody MemberDto.Patch patch) {
        patch.setMemberId(memberId);
        Member updatedMember = memberService.updateMember(memberMapper.memberPatchDtoToMember(patch));
        return new ResponseEntity(memberMapper.memberToMemberResponseDto(updatedMember), HttpStatus.OK);
    }

    @GetMapping("{member-id}")
    public ResponseEntity getMember (@PathVariable ("member-id") long memberId) {
        Member findMember = memberService.findMember(memberId);
        return new ResponseEntity(memberMapper.memberToMemberResponseDto(findMember), HttpStatus.OK);
    }

    @DeleteMapping("{member-id}")
    public ResponseEntity deleteMember (@PathVariable ("member-id") long memberId) {
        memberService.deleteMember(memberId);
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

Controller클래스 입니다. 클라이언트를 통해 요청받은 데이터들은 Controller클래스를 통해 각 영역에 전달되게 됩니다.

@RestController
해당 클래스라 Controller클래스라는 것을 알립니다.

@RequestMapping("/members")
도메인 주소를 나타냅니다. 애플리케이션을 실행시키면 localhost:8080이 기본 도메인이 됩니다. localhost:8080/members 를 통해 회원과 관련 된 로직을 활용할 수 있다고 볼 수 있습니다.

@PostMapping("/signup)
Post요청을 보낼 영역임을 나타냅니다. CRUD의 Create 에 해당하는 부분이라 볼 수 있습니다. 로직 내 @RequestBody는 DTO로 전달될 값을 JSON타입으로 보내겠다는 뜻입니다.

@PatchMapping("{member-id})
Patch요청을 보낼 영역입니다. CRUD의 Update입니다. member-id는 Member의 PK값으로 회원 구분을 하겠다는 것이고 @PathVariable은 /member/ 뒤에 PK값으로 회원구분을 해서 요청을 처리한다는 의미 입니다.
예를들어 PK값을 1로 가지고있는 회원의 정보를 수정하려고 한다면 localhost:8080/members/1 이쪽으로 요청을 보내면 됩니다.

@GetMapping("{member-id})
Get요청을 보낼 영역입니다. CRUD 의 Read 에 해당합니다.

@DeleteMapping("{member-id})
Delete요청을 보낼 영역입니다. CRUD의 Delete 입니다.

 

스키마 구성

CREATE TABLE IF NOT EXISTS MEMBER (
    member_id bigint NOT NULL AUTO_INCREMENT,
    email varchar(100) NOT NULL,
    password varchar(255) NOT NULL,
    nickname varchar(100) NOT NULL,
    PRIMARY KEY (member_id)
);

schema.sql 파일 입니다. src → resources → db → h2 패키지에 생성했습니다. resources패키지 안에만 들어가있으면 됩니다. 해당 파일을 통해 테이블과 컬럼을 생성하게 되고 클라이언트를 통해 요청받은 데이터들이 DB에 들어가게 됩니다.

 

yml 구성

spring:
  h2:
    console:
      enabled: true
      path: /h2
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
  sql:
    init:
      schema-locations: classpath*:db/h2/schema.sql
  jpa:
    hibernate:
      ddl-auto: none
    datasource:
      initialization-mode: always
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true

yml파일 구성 입니다. 이전 기본설정에서도 진행했었는데 db를 불러올 위치를 추가했습니다. 저와 패키지를 다르게 구성하셨다면 본인의 구성에 맞게 변경하여 사용하면 됩니다. yml은 간격이 매우매우 중요합니다!

 

Postman을 사용한 테스트

결과물이 잘 작동하는지 테스트를 진행하겠습니다. 테스트를 위해 사용한 파일은 포스트맨 입니다. 클라이언트의 요청을 보내주는 역할을 하는 프로그램 입니다.

회원 생성

회원정보 수정

회원정보 조회 (실수로 1번 회원을 지워서 2번을 생성해서 조회했습니다)

회원 삭제

 

localhost:8080/h2 로 접속하면 데이터가 어떻게 바뀌는지 직접 볼 수 있습니다.