✅ 이전 포스팅
2024.11.06 - [Java/쇼핑몰] - 쇼핑몰 - 3 [GitHub Actions를 통한 CI/CD]
쇼핑몰 - 3 <GitHub Actions를 통한 CI/CD>
✅ 이전 포스팅2024.11.03 - [Java/쇼핑몰] - 쇼핑몰 - 2 " data-og-description="✅ 이전 포스팅2024.11.01 - [Java/쇼핑몰] - 쇼핑몰 만들기 - 1 " data-og-description="✅ 동기예에에에에에전에 만들었던 허접한 게시판
magicmk.tistory.com
이전 포스팅에서는 최종적으로 CI/CD를 구성하여 프로젝트의 배포를 편이 하도록 만들었다.
이번 포스팅부터는 본격적인 코드 구성을 작성하려고 한다.
쇼핑몰 포스팅에서는 공통 예외처리, 프로젝트 구조 등은 제외하고 기능 구현만 기술합니다.
✅ DB 설계
프로젝트를 생성하기 앞서 중요한 것은 DB 설계가 아닐까 싶다.
프론트엔드는 화면을 퍼블리싱하기 전에 디자인이 제대로 갖춰줘야 하는 것처럼
백엔드는 DB가 제대로 설계되어 있어야 개발하는데 용이한 것 같다.
https://www.erdcloud.com/d/NhQeqSxtmF3Yz5NFQ
쇼핑몰
Draw ERD with your team members. All states are shared in real time. And it's FREE. Database modeling tool.
www.erdcloud.com
본인은 우선 위에 나와있는데로 설계를 했는데 굉장히 굉장히 간단하게 만들었다.
물론 테이블과 컬럼들이 항상 추가되거나 삭제될 수 있다.
쇼핑몰 포스팅은 완성한 후 작성하는 것이 아니기 때문에 ERD와 코드들이 지속적으로 변경될 수 있다.
✅ Domain 구현
우선 DB 설계에 맞게 도메인을 구현한다.
🟨 BaseEntity
package toy.shop.domain;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
BaseEntity는 작성일자, 수정일자와 같이 대부분의 도메인에서 사용하는 항목을 적어둔 것이다.
🟨 Role
package toy.shop.domain;
import lombok.Getter;
@Getter
public enum Role {
ROLE_USER("ROLE_USER"),
ROLE_COMPANY("ROLE_COMPANY"),
ROLE_ADMIN("ROLE_ADMIN");
private final String role;
Role(String role) {
this.role = role;
}
}
Role의 경우 enum으로 회원의 권한을 담는 정보이다.
🟨 Member
package toy.shop.domain.member;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import toy.shop.domain.BaseEntity;
import toy.shop.domain.Role;
@Entity
@Getter
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Setter
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String nickName;
@Column(nullable = false)
private String gender;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Role role;
@Column(nullable = false)
private String imagePath;
@Column
private String phoneNumber;
@Column(nullable = false)
@ColumnDefault("'N'")
private char banType = 'N';
@Column(nullable = false)
@ColumnDefault("'N'")
private char deleteType = 'N';
@Builder
public Member(String email, String password, String nickName, String gender, Role role, String imagePath, String phoneNumber) {
this.email = email;
this.password = password;
this.nickName = nickName;
this.gender = gender;
this.role = role;
this.imagePath = imagePath;
this.phoneNumber = phoneNumber;
}
}
Member 도메인 또한 DB 설계에 맞게 잘 생성해 준다. password에만 @Setter가 있는 이유는 추후 비밀번호 변경
로직에 사용하기 위해서다.
✅ 회원가입
회원가입은 전달 받은 정보를 통해 DB에 값을 넣기만 하면 되기 때문에 간단하다.
🟨 DTO
package toy.shop.dto.member;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import toy.shop.domain.Role;
@Data
@NoArgsConstructor
@Schema(description = "회원가입에 필요한 요청 정보", requiredProperties = {"email", "password", "nickname", "gender", "role", "phone"})
public class SignupRequestDTO {
@Schema(description = "회원 이메일")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String email;
@Schema(description = "회원 비밀번호")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String password;
@Schema(description = "회원 닉네임")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String nickname;
@Schema(description = "회원 성별", example = "W or M")
@NotBlank(message = "해당 값은 필수값 입니다.")
@Pattern(regexp = "W|M", message = "해당 값은 W 또는 M 이어야 합니다.")
private String gender;
@Schema(description = "회원 권한", example = "ROLE_USER or ROLE_COMPANY")
@NotNull(message = "해당 값은 필수값 입니다.")
private Role role;
@Schema(description = "휴대번호")
@NotBlank(message = "해당 값은 필수값 입니다.")
private String phone;
@Builder
public SignupRequestDTO(String email, String password, String nickname, String gender, Role role, String phone) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.gender = gender;
this.role = role;
this.phone = phone;
}
}
Schema의 경우는 Swagger에 설명을 붙이기 위해 작성했기 때문에 swagger를 사용하지 않는다면 필요 없다.
🟨 Controller
package toy.shop.controller.global;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import toy.shop.dto.Response;
import toy.shop.dto.member.SignupRequestDTO;
import toy.shop.service.member.MemberService;
import static toy.shop.controller.ResponseBuilder.buildResponse;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController implements AuthControllerDocs {
private final MemberService memberService;
@PostMapping("/sign-up")
public ResponseEntity<Response<?>> joinMember(@RequestBody @Valid SignupRequestDTO parameter) {
Long result = memberService.signup(parameter);
return buildResponse(HttpStatus.CREATED, "회원가입 성공", result);
}
}
컨트롤러에서는 MemberService를 가져와서 호출하고 클라이언트에게 적절하게
Response 값을 전달하는 것만 들어있다.
AuthControllerDocs는 API 문서를 위해 Swagger에서 보여줄 SpringDoc를 작성한 것이니 필요하면
검색해서 적용해 보길 바란다.
🟨 Service
package toy.shop.service.member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import toy.shop.cmmn.exception.ConflictException;
import toy.shop.domain.member.Member;
import toy.shop.dto.member.SignupRequestDTO;
import toy.shop.repository.member.MemberRepository;
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
@Value("${path.profileImage}")
private String profileImagePath;
/**
* 사용자의 회원가입을 처리하는 메서드입니다.
*
* @param parameter 회원가입 요청 정보를 담고 있는 SignupRequestDTO 객체입니다.
* @return 저장된 회원의 ID를 반환합니다.
* @throws ConflictException 이메일이 이미 존재하는 경우 예외가 발생합니다.
*
* 이 메서드는 다음 작업을 수행합니다:
* 1. 이메일 중복 체크: 입력된 이메일이 이미 존재하는 경우 ConflictException이 발생합니다.
* 2. 새로운 회원 생성: 주어진 정보를 사용하여 새로운 회원 객체를 생성하고 암호화된 비밀번호를 설정합니다.
* 3. 회원 정보 저장: 생성된 회원 객체를 저장하고, 저장된 회원의 ID를 반환합니다.
*/
public Long signup(SignupRequestDTO parameter) {
memberRepository.findByEmail(parameter.getEmail())
.ifPresent(existingMember -> {
throw new ConflictException("이미 존재하는 이메일입니다: " + parameter.getEmail());
});
Member member = Member.builder()
.email(parameter.getEmail())
.password(passwordEncoder.encode(parameter.getPassword()))
.nickName(parameter.getNickname())
.gender(parameter.getGender())
.role(parameter.getRole())
.imagePath(profileImagePath + "/anonymous.png")
.phoneNumber(parameter.getPhone())
.build();
return memberRepository.save(member).getId();
}
}
서비스 로직에 대한 설명은 Javadoc을 통해서 간략하게 작성해 뒀으니 이해하는데 문제는 없을 것 같다.
🟨 Repository
package toy.shop.repository.member;
import org.springframework.data.jpa.repository.JpaRepository;
import toy.shop.domain.member.Member;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
/**
* 이메일 주소를 기반으로 회원 정보를 조회하는 메서드입니다.
*
* @param email 조회할 회원의 이메일 주소
* @return 주어진 이메일 주소를 가진 회원을 Optional로 반환하며, 존재하지 않으면 Optional.empty()를 반환합니다.
*/
Optional<Member> findByEmail(String email);
}
늘 느끼지만 JPA는 감동 그 잡채...
🟨 Test
package toy.shop.service.member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import toy.shop.cmmn.exception.ConflictException;
import toy.shop.domain.Role;
import toy.shop.dto.member.SignupRequestDTO;
import toy.shop.repository.member.MemberRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Transactional
@SpringBootTest
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
@DisplayName("정상 가입 테스트")
public void signup() {
SignupRequestDTO dto = SignupRequestDTO.builder()
.email("test@test.com")
.password("1234")
.gender("M")
.nickname("test")
.role(Role.ROLE_USER)
.phone("010-1234-5678")
.build();
Long result = memberService.signup(dto);
assertThat(result).isEqualTo(memberRepository.findByEmail("test@test.com").get().getId());
}
@Test
@DisplayName("중복 가입 테스트")
public void signupConflictEmail() {
SignupRequestDTO dto = SignupRequestDTO.builder()
.email("test@test.com")
.password("1234")
.gender("M")
.nickname("test")
.role(Role.ROLE_USER)
.phone("010-1234-5678")
.build();
SignupRequestDTO dto2 = SignupRequestDTO.builder()
.email("test@test.com")
.password("1234")
.gender("M")
.nickname("test")
.role(Role.ROLE_USER)
.phone("010-1234-5678")
.build();
Long result = memberService.signup(dto);
assertThrows(ConflictException.class, () -> memberService.signup(dto2));
}
}
서비스 로직의 테스트 코드를 작성한 뒤 실행 시켜보면
정상적으로 잘 동작한다.
✅ 마무리
원래 이번 포스팅에서 회원가입과 spring security + jwt + redis를 이용한 로그인까지 함께 작성하려고 했으나
생각보다 양이 많아져서 로그인은 별도로 작성해야 할 것 같다.
이번 프로젝트를 진행하면서 테스트 코드도 함께 작성하기 시작했는데 뭔가 엉성한 것 같지만
테스트 코드가 있어서 빌드 전에 컴파일 오류를 확인할 수 있어 좋은 것 같다.
https://github.com/sideProject-org/shop-back
GitHub - sideProject-org/shop-back: 쇼핑몰 서버
쇼핑몰 서버. Contribute to sideProject-org/shop-back development by creating an account on GitHub.
github.com
'Java > 쇼핑몰' 카테고리의 다른 글
쇼핑몰 - 6 [Spring Security + JWT + OAuth2 + Redis] (2) | 2024.12.02 |
---|---|
쇼핑몰 - 5 [spring security + JWT + Redis 로그인] (0) | 2024.11.19 |
쇼핑몰 - 3 [GitHub Actions를 통한 CI/CD] (3) | 2024.11.06 |
쇼핑몰 - 2 [Docker-compose 사용하기] (0) | 2024.11.03 |
쇼핑몰 만들기 - 1 [AWS EC2로 배포 서버 만들기] (3) | 2024.11.01 |