팀프로젝트일기/SKKUNION

Spring 프로젝트(SKKUNION) 폴더 생성하기 및 이유

Recfli 2023. 9. 23. 15:43

스프링의 작업 방식

 스프링의 Layered Architecture이라고 불리는 부분인데 스프링은 Controller, Service, Repository 3개로 나뉘어져있습니다. 그리고 추가적으로 Entity, Dto가 있고 이 외에 Security, filter, Config, Exception 이런 게 있는데 이 부분은 자세한 설명을 제외하고 나머지 5개의 각각의 역할에 대해서 우선 이유를 말씀드릴게요.

 

Entity

 

대부분 작업을 시작하면 Entity부터 작업을 하는 게 우선이라, Entity부터 말씀드릴게요. Entity는 DB의 Table을 Java 객체로 표현한 부분이라고 생각을 하시면 돼요. 그래서 DB Table과 거의 동일하게 설계가 됩니다.

 

그런데 문제점이 몇 개가 있어요. 우선 Java 객체를 설계하는 방식과 DB에서 table을 설계하는 방식이 우선 달라요. 이름부터가 그렇죠? 예시로 그냥 로그인 시스템을 혼자 공부했을 때 사용했던 자료를 사용할게요.

 

 우선 테이블에서는 각각의 Column 명칭이 _으로 구분을 해요. Primary key도 있고 외부 Table과 Join해야하는 경우에는 Foreign Key도 있고 합니다. 이 테이블엔 없네요. 

 

 Foreign Key는 댓글이나 게시글 Table 설계를 한 후에 제가 정리해서 올리겠습니다. Spring 카테고리에 가시면 정리된 내용을 보실 수도 있어요.

CREATE TABLE User(
	user_email VARCHAR(50) PRIMARY KEY,
    user_password VARCHAR(200) NOT NULL,
    user_nickname VARCHAR(30) NOT NULL,
    user_phone_number VARCHAR(15) NOT NULL,
    user_address TEXT NOT NULL,
    user_profile TEXT
);

 

 이제 다시 위 Table을 자바 객체로 다시 변경해서 보면 아래처럼 나와요. 일단 Java에서는 다른 인스턴스에서 private 데이터를 꺼내기 위해서 getter, getter 같은 게 필요하니 ArgsConstructor 부분이 들어갔고요. DB의 table과 Mapping이 되는 객체임을 알리기 위해서 Entity가 들어갔고요. Id는 primary key라는 걸 나타내고 명칭이 다릅니다.

 

 table에서는 user_email로 스네이크 케이스로 되어있는데 Java에서는 카멜케이스로 userEmail로 되어있어요. 이거 진짜 조심해야 됩니다.  자체적으로 JPA로 설계를 하는 경우에 CRUD를 정말 편하게 할 수 있지만 Table 명칭도 Java 내부에 있는 기본 함수랑 이름이 같아버리는 경우에 문제가 생길 수도 있고요. 저거 카멜케이스를 JPA에서 대문자 기준으로 나눠서 스네이크 케이스로 변경해서 쿼리를 만들어주기 때문에 안 지키고 만드시면 에러는 뜨는데 무슨 이유인지도 모른 채로 몇 시간 동안 고민하는 경험을 하실 수 있어요.

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name="User")
@Table(name="User")
public class UserEntity {

    @Id
    private String userEmail;
    private String userPassword;
    private String userNickname;
    private String userPhoneNumber;
    private String userAddress;
    private String userProfile;

    public UserEntity(SignUpDto dto){
        this.userEmail = dto.getUserEmail();
        this.userPassword = dto.getUserPassword();
        this.userNickname = dto.getUserNickname();
        this.userPhoneNumber = dto.getUserPhoneNumber();
        this.userAddress = dto.getUserAddress() + " " + dto.getUserAddressDetail();
    }
}

 이 외에도 조심해야할 내용이 많은데 그 내용들은 해당 내용을 구현할 때에 설계가 완성되면 뒤에 다시 설명을 드리겠습니다.

 

Dto

 사실 이 부분을 제가 정확한 이유는 모르겠습니다. Entity도 있는데 특정 로직에 필요한 데이터만 따로 구분해서 사용을 하려고 하는 것 같아요. 그러니까 응답 형태를 상화에 맞게 정리해놓는거라고 생각하시면 될 것 같아요. Kotlin을 사용해본 적이 있으실지는 모르겠지만, 코틀린에서 listView에서 데이터를 뿌릴 때 인터페이스 사용하려면 data class를 따로 만들어야지만 뿌려졌던 것처럼 똑같은 건데 Java에는 enum class는 있지만 Data class는 없으니까 이 부분을 따로 Java 개발자 분들이 따로 폴더로 구분해놓는 것 같아요.

 

 아래 코드는 클라이언트에서 로그인 요청을 위해서 서버에 userEmail과 userPassword를 바디에 넣어서 보내고 서버에서 로그인이 성공하면 클라이언트에게 cookie 생성을 위해서 주는 데이터에요. 이렇게 응답 형식을 정리하기 위한 설계 방식인 것 같습니다. 로직에 맞게 따로 정리하는 게 나중에 디버깅하기도 편하고 service에서 실제로 구현하고 Response를 만들었을 때에도 따로 구분하기 편하고요. 제 생각엔 필요한 부분인 것 같습니다. 

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignInResponseDto {

    private String token;
    private int exprTime;
    private UserEntity user;

}

 

Repository

 Repository는 DB에 요청할 내용을 만들어놓는 장소입니다. 일반적으로 Spring JPA를 사용하지 않았을 때에 데이터를 꺼내올 때에는 Query를 작성해서 꺼내오셨을텐데 JPA를 사용하면 Query를 알아서 생성해줍니다. 원하는 쿼리를 만들어주는 함수를 저장하는 곳을 Repository라고 생각하시면 돼요. 

 

 좀 복잡한 경우에는 직접 쿼리도 지원을 하니 영 불편하면 직접 MySQL 문법에 맞게 작성을 해주셔도 되고요. 나중에 해당 요청에 따라 쿼리가 어떻게 날라가는지, DB 자동생성 및 내리는 거에 관한 옵션을 따로 하나 페이지 만들어서 올릴게요. 그걸 잘 보시면 될 것 같습니다.

 

간단하게 조회 및 저장, 삭제만 아래처럼 하면 돼요. 자세한 내용 및 다양한 옵션은 spring JPA 공식문서를 참조해주시면 감사하겠습니다.

https://docs.spring.io/spring-data/jpa/docs/2.7.16/reference/html/#jpa.entity-persistence

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    // And를 통해서 여러 column에서 해당하는 조건에 해당하는 값을 찾는 방식도 있습니다.
    public boolean existsByUserEmailAndUserPassword(String userEmail, String userPassword);
    // 하나 Column으로만 찾는 것도 가능하고요.
    public UserEntity findByUserEmail(String userEmail);
    // 삭제도 가능합니다.
    public void deleteInUserEmail(String userEmail);
    // 이렇게 저장도 가능하고요.
    public String save(UserEntity userEntity);
}

사실 Save는 저거 Repository 어노테이션 달아주면 알아서 되는 건데 그냥 넣어놨습니다.

 

Service

 서비스는 이때까지 만든 직접 복잡한 부분을 구현하는 부분이라고 생각하시면 돼요. 예를 들면 회원가입을 한다고 가정을 해보시면 여러 가지 체크해볼 사항이 있을거에요.

 

1. 데이터 베이스가 제대로 돌아가고 있는가?

2. 데이터 베이스가 돌아가더라도 Table 형식과 맞지 않은 데이터가 들어오고 있지는 않은가? 프론트에서도 어느정도 잡아줄 수는 있지만 비밀번호를 VARCHAR(20)으로 놓고 암호화를 해버리면 너무 길어져서 에러가 나는 경우가 있으니 이런 부분을 조심하셔야 됩니다.

3. 회원 가입의 경우에 비밀번호와 check가 둘이 같은가? 아니면 이미 있는 데이터가 아닌가? 등을 모두 체크해야 합니다.

4. 응답의 결과로 어떤 데이터값을 반환할까?

 

 이런 부분을 모두 구현하는 부분이 Service에요. 그리고 위에서 사용하는 문제의 경우를 제대로 구분하고 제대로 실행이 되지 않는 경우를 위해서 보통 각각의 상황에마다 예외 및 예외 문구 (Exception)을 만들어서 실행을 하면 디버깅하기가 훨신 쉽습니다. 예시 코드 하나 올릴게요. 이해가 잘 안되시면 Java의 와일드 카드와 예외처리 부분을 다시 공부해보시면 이해할 수 있을 겁니다.

    public ResponseDto<?> signUp(SignUpDto dto){
        String userEmail = dto.getUserEmail();
        String userPassword = dto.getUserPassword();
        String userPasswordCheck = dto.getUserPasswordCheck();

        // email 중복 확인
        try{
            if(userRepository.existsById(userEmail))
                return ResponseDto.setFailed("Existed Email!");
        } catch(Exception e){
            return ResponseDto.setFailed("Data Base Error1!");
        }

        // 비밀번호가 서로 다르면 failed response 반환!
        if(!userPassword.equals(userPasswordCheck))
            return ResponseDto.setFailed("password does not matched!");

        // UserEntity 생성
        UserEntity userEntity = new UserEntity(dto);

        // 비밀번호 암호화
        String encodedPassword = passwordEncoder.encode(userPassword);
        System.out.println(encodedPassword);
        userEntity.setUserPassword(encodedPassword);

        try{
            // userRepository를 이용해서 데이터베이스에 Entity 저장
            userRepository.save(userEntity);
        } catch(Exception e){
            return ResponseDto.setFailed("Data Base Error2");
        }

        // 성공시 success Response 반환
        return ResponseDto.setSuccess("SignUp Success!", null);
    }

 

Controller

 이제 Entity로 DB의 table과 객체를 어떻게 mapping하는지 JPA로 쿼리를 만드는 부분 Repository, 실제로 구현하는 부분 Service, 데이터 타입 객체 Dto 이렇게 모든 준비가 되었으면 이제 웹에 이걸 JSON형태로 제공을 해줘야겠죠. 그 부분을 담당하는게 Controller입니다. 직접 보는게 편할 것 같아서 코드부터 보면 이해가 빠를 겁니다.

 

 우선 Dto 코드는 아래와 같아요. 해당 코드가 잘 이해가 안되시면 Java에서 지네릭스를 찾아보시고 공부하시면 이해가 될 겁니다.

@Data
@AllArgsConstructor(staticName="set")
public class ResponseDto<D> {

    private boolean result;
    private String message;
    private D data;

    // 성공했을 때 정의하는 메서드
    public static <D> ResponseDto<D> setSuccess(String message, D data){
        return ResponseDto.set(true, message, data);
    }

    // 실패했을 때 정의하는 메서드
    public static <D> ResponseDto<D> setFailed(String message){
        return ResponseDto.set(false, message, null);
    }
}

 

 아래는 로그인과 관련된 controller에요. RequestMapping은 내가 Auth와 관련된 부분은 해당 url과 매핑을 하겠다는 의미입니다. @Autowired는 DI랑 관련이 있는 부분인데, 이 부분은 spring 카테고리에 MVC 패턴 공부하면서 이해한 부분을 주말 안에 정리할 예정이니 해당 내용을 봐주세요. Config 폴더가 왜 필요한지도 해당 부분을 이해하시면 알 수 있어요.

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired AuthService authService;

    @PostMapping("/signUp")
    public ResponseDto<?> signUp(@RequestBody SignUpDto requestBody){
        ResponseDto<?> result = authService.signUp(requestBody);
        return result;
    }

    @PostMapping("/signIn")
    public ResponseDto<SignInResponseDto> signIn(@RequestBody SignInDto requestBody){
        ResponseDto<SignInResponseDto> result = authService.signIn(requestBody);
        return result;
    }
}

 

 RestController에 대한 정확한 이해는 없지만 제 생각에는 RequestBody 어노테이션과 해당 값을 REST api형태로 해서 ReponseBody를 응답으로 클라이언트에게 전송하겠다는 의미같아요. 이건 서버를 키고 포스트맨으로 해당 내용을 제가 한번 보여드리겠습니다.

 

우선, DB에 user_email = "qwer@qwer", user_password = '"qwer"로 User 테이블에 저장이 되어있어요. 그렇기 때문에 signUp에서 이제 해당 데이터 값을 JSON형태로 body로 보내면 responseDto에서는 응답이 성공했는지 실패했는지와 해당 메세지, 보내는 Data값 이렇게 ReponseBody에 JSON형태로 담아서줘요. SignIn부분이 더 복잡해서 올리지는 않았지만 아래 결과를 보시면 바로 이해되실겁니다. 

 

 일단, 제가 계속 공부하려고 수정하다가;; 뭘 잘못 건드려서 안되는 것 같은데 일단 제가 배운 강의영상 캡처본을 올렸습니다. 

 

폴더생성

 그래서 앞으로 시작을 할 때 저는 해당 폴더를 미리 전부 만들어놓고 시작을 하겠습니다. 나중에 쓸 일도 있으니까요. 그래서 폴더 생성을 다음과 같은 이미지로 전 시작을 할 것 같아요.