Spring - User관리 1.기초세팅

임재현·2021년 5월 18일

SPRING

목록 보기
1/5

RESTful Web Services, Java, Spring Boot, Spring MVC and JPA 를 보며 정리하는 글

내 깃헙
먼저 나는 자바 1.8버젼을 설치했고(설치방법은 TIPS에 올려놨다.), STS4(Spring Tool Suit) IDE를 이용해 작업하고 있다. DB는 MySQL이다.

STS에서 File - Spring Starter Project로 새로운 프로젝트를 만들게 되면 처음에 프로젝트 이름 등 을 작성하고, 의존성관리는 maven으로. next -> Dependency설정에서 spring Boot 2.4.5버젼을 선택하고, Web을 검색해 Spring Web을 선택하고 next -> 새로운 프로젝트를 만들어주었다.

그리고 library들을 관리하는 pom.xml로 들어가보면

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

이렇게 2개의 dependecy가 추가되어있는 것을 볼 수 있다. 여기에다가

		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>

이렇게 3개의 디펜던시를 더 추가해준다. 디펜던시를 추가할 때, 주로 maven repository에서 검색해서 추가하게 될 텐데, 이 때 주의사항이 version정보는 빼준다. spring boot에서 알아서 관리해주기 때문이다. 추가한 디펜던시는 위에서 부터 jpa(java에서 사용하는 orm중 하나), mysql connector, 그리고 마지막꺼는 http response를 보낼 때 에러메세지를 보낼 수 있게 해주는 디펜던시다.

그리고 src/main/resources디렉토리로 들어가보면 application.properties가 있는데, 여기에 db의 접속정보 등을 입력한다. node의 .env의 역할과 비슷한 것 같다.

spring.datasource.username=UserName
spring.datasource.password=Passowrd
spring.datasource.url=DB URL
spring.jpa.hibernate.ddl-auto=update

.gitignore에 등록해두는 게 좋을 듯하다.

그리고 현재 회원가입할 때 DB에 INSERT 하는 것 까지 만들었는데, 파일구조는 다음과 같다.

├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── appdeveloperblog
│   │   │           └── app
│   │   │               └── ws
│   │   │                   ├── MobileAppWsApplication.java
│   │   │                   ├── UserRepository.java
│   │   │                   ├── io
│   │   │                   │   └── entity
│   │   │                   │       └── UserEntity.java
│   │   │                   ├── service
│   │   │                   │   ├── UserService.java
│   │   │                   │   └── impl
│   │   │                   │       └── UserServiceImpl.java
│   │   │                   ├── shared
│   │   │                   │   ├── Utils.java
│   │   │                   │   └── dto
│   │   │                   │       └── UserDto.java
│   │   │                   └── ui
│   │   │                       ├── controller
│   │   │                       │   └── UserController.java
│   │   │                       └── model
│   │   │                           ├── request
│   │   │                           │   └── UserDetailsRequestModel.java
│   │   │                           └── response
│   │   │                               └── UserRest.java
│   │   └── resources
│   │       ├── application.properties
│   │       ├── static
│   │       └── templates
│   └── test
│       └── java
│           └── com
│               └── appdeveloperblog
│                   └── app
│                       └── ws
│                           └── MobileAppWsApplicationTests.java
└── target
    ├── classes
    │   ├── META-INF
    │   │   ├── MANIFEST.MF
    │   │   └── maven
    │   │       └── com.appdeveloperblog.app.ws
    │   │           └── mobile-app-ws
    │   │               ├── pom.properties
    │   │               └── pom.xml
    │   ├── application.properties
    │   └── com
    │       └── appdeveloperblog
    │           └── app
    │               └── ws
    │                   ├── MobileAppWsApplication.class
    │                   ├── UserRepository.class
    │                   ├── io
    │                   │   └── entity
    │                   │       └── UserEntity.class
    │                   ├── service
    │                   │   ├── UserService.class
    │                   │   └── impl
    │                   │       └── UserServiceImpl.class
    │                   ├── shared
    │                   │   ├── Utils.class
    │                   │   └── dto
    │                   │       └── UserDto.class
    │                   └── ui
    │                       ├── controller
    │                       │   └── UserController.class
    │                       └── model
    │                           ├── request
    │                           │   └── UserDetailsRequestModel.class
    │                           └── response
    │                               └── UserRest.class
    └── test-classes
        └── com
            └── appdeveloperblog
                └── app
                    └── ws
                        └── MobileAppWsApplicationTests.class

뭔가 엄청 많아보이지만 건드린 부분은 src/main/java부분밖에 없다.

이제 하나씩 살펴보면 먼저 com.appdeveloperblog.app.ws패키지의 MobileAppWsApplication.java클래스의 경우 기본 생성 클래스이다. 이름이 MobileAppWsApplication.java인 이유는 이 프로젝트의 이름이 MobileAppWs이기 때문이다. 어쨌든 이 클래스는 다음과 같이 생겼다.

package com.appdeveloperblog.app.ws;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MobileAppWsApplication {

	public static void main(String[] args) {
		SpringApplication.run(MobileAppWsApplication.class, args);
	}

}

앱을 run하면 가장 먼저 실행되는 Main Class이다.

다음으로, com.appdeveloperblog.app.ws.ui.controller 패키지를 만들어주고, UserController클래스를 만들어줬다. 현재까지 작성된 내용은 다음과 같다.

package com.appdeveloperblog.app.ws.ui.controller;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.appdeveloperblog.app.ws.service.UserService;
import com.appdeveloperblog.app.ws.shared.dto.UserDto;
import com.appdeveloperblog.app.ws.ui.model.request.UserDetailsRequestModel;
import com.appdeveloperblog.app.ws.ui.model.response.UserRest;

@RestController
@RequestMapping("users")
public class UserController {
	
	@Autowired
	UserService userService;

	@GetMapping
	public String getUser() {
		return "get User is called";
	}
	
	@PostMapping
	public UserRest createUser(@RequestBody UserDetailsRequestModel userDetails) {
		UserRest returnValue = new UserRest();
		
		UserDto userDto = new UserDto();
		BeanUtils.copyProperties(userDetails, userDto);
		
		UserDto createdUser = userService.createUser(userDto);
		BeanUtils.copyProperties(createdUser, returnValue);
		
		return returnValue;
	
	}
	
	@PutMapping
	public String updateUser() {
		return "update user was called";
	}
	
	@DeleteMapping
	public String deleteUser() {
		return "delete user was called";
	}
}
  • @RestController는 이 클래스를 Rest API를 다루는 컨트롤러로 사용할 것(사실 그냥 http request처리)을 의미한다.
  • @RequestMapping("users")는 http://localhost:8080/users처럼 users일 때 작동한다고 보면 되겠다. Node.JS - express 에서 Router와 비슷한 역할이라고 보면 되겠다.
  • @Autowired는 다른 클래스들에 @Service, @Component, 현재 이 UserController에서는 @RestController 처럼 Bean으로 등록되어 있는 클래스들을 싱글톤으로 가져와 사용하기위해 사용한다.
  • @GetMapping,@PostMapping,@PutMapping,@DeleteMapping은 각각 Get, Post, Put, Delete요청이 들어왔을 때 그 메서드를 사용하라는 의미이다.

여기까지만 작성하고, 서버를 run하고(오류가 있으면 TIPS에 적어놓은 글 참조) POST MAN등으로 http요청을 보내면 각각 그에 맞는 응답을 해주는 것을 볼 수 있다.

여기서는 createUser메서드를 많이 작성했으므로 createUser메서드를 살펴보겠다.
@RequestBody는 http Request의 Body부분을 의미한다. 즉, createUser메서드에서는 httpRequest의 Body부분을 UserDetailsRequestModel형식의 인자로 받고 있다.

이제 여기서 UserRequestModel을 살펴보면 com.appdeveloperblog.app.ws.ui.model.request 패키지에 UserDetailsRequestModel클래스로 만들어 주었다.http Requst를 다루는 VO클래스라고 생각하면 편할 것 같다.(VO(Value Object) : DB에 있는 테이블 컬럼 값을 java에서 객체로 다루기 위해 사용함)

package com.appdeveloperblog.app.ws.ui.model.request;

/**
 * @author limjaehyeon
 *
 */
public class UserDetailsRequestModel {

	private String firstName;
	private String lastName;
	private String email;
	private String password;
	
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	
	
}

그리고 Response를 다루기 위한 VO클래스도 따로 만들었는데, com.appdeveloperblog.app.ws.ui.model.response패키지의 UserRest클래스이다.

package com.appdeveloperblog.app.ws.ui.model.response;

public class UserRest {
	private String userId;
	private String firstName;
	private String lastName;
	private String email;
	
	public String getUserId() {
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	
	
}

이쯤에서 다시 createUser메서드를 다시 보자.

@PostMapping
	public UserRest createUser(@RequestBody UserDetailsRequestModel userDetails) {
		UserRest returnValue = new UserRest();

		UserDto userDto = new UserDto();
		BeanUtils.copyProperties(userDetails, userDto);

		UserDto createdUser = userService.createUser(userDto);
		BeanUtils.copyProperties(createdUser, returnValue);

		return returnValue;
	
	}

UserDto는 package com.appdeveloperblog.app.ws.shared.dto패키지에 있는 UserDto클래스로, Serializable 을 implemnts하고 있다. DTO는 Data Transfer Object로서 계층간 데이터 교환을 위한 객체(Java Beans)이다.DB에서 데이터를 얻어 Service나 Controller 등으터 보낼 때 사용하는 객체를 말한다.즉, DB의 데이터가 Presentation Logic Tier로 넘어오게 될 때는 DTO의 모습으로 바껴서 오고가는 것이다.[DAO] DAO, DTO, Entity Class의 차이
heejeong Kwon 25 Dec 2018
또 , Serializable은 자바 직렬화를 위한 것, 쉽게 말하면 데이터타입을 맞추기 쉽게하기 위한 것이다.Java의 직렬화(Serialize)란?참조
UserDto클래스는 다음과 같다.

package com.appdeveloperblog.app.ws.shared.dto;

import java.io.Serializable;

public class UserDto implements Serializable{

	private static final long serialVersionUID = -7483311878566055818L;
	private long id;
	private String userId;
	private String firstName;
	private String lastName;
	private String email;
	private String password;
	private String encryptedPassword;
	private String emailVerificationToken;
	private Boolean emailVerificationStatus = false;
	
	public long getId() {
		return id;
	}
	public void setId(long id) {
		this.id = id;
	}
	public String getUserId() {
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getFirstName() {
		return firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
	public String getLastName() {
		return lastName;
	}
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getEncryptedPassword() {
		return encryptedPassword;
	}
	public void setEncryptedPassword(String encryptedPassword) {
		this.encryptedPassword = encryptedPassword;
	}
	public String getEmailVerificationToken() {
		return emailVerificationToken;
	}
	public void setEmailVerificationToken(String emailVerificationToken) {
		this.emailVerificationToken = emailVerificationToken;
	}
	public Boolean getEmailVerificationStatus() {
		return emailVerificationStatus;
	}
	public void setEmailVerificationStatus(Boolean emailVerificationStatus) {
		this.emailVerificationStatus = emailVerificationStatus;
	}
	
	
}

다음으로 BeanUtils.copyProperties()는 스프링에서 제공하는 함수인데, setter를 더 편리하게 사용할 수 있게해주는 함수이다.코딩시그널 - [SPRING] BeanUtils.copyProperties을 이용하여 Class간의 property 복사하기

즉,

BeanUtils.copyProperties(userDetails, userDto);

의 의미는

userDto.setUser(userDetails.getUser()),
userDto.setEmail(userDetails.getEmail()),
userDto.setPassword(userDetails.getPassword())

의 의미와 같은것이다.

다음으로 UserService이다. UserService는 인터페이스로, com.appdeveloperblog.app.ws.service패키지안에 있고, 내용은

package com.appdeveloperblog.app.ws.service;

import com.appdeveloperblog.app.ws.shared.dto.UserDto;

public interface UserService {
	UserDto createUser(UserDto user);
}

이다. 그리고 userService인터페이스를 com.appdeveloperblog.app.ws.service.impl패키지의 UserServiceImpl클래스가 implements해서 구현하고 있다.

package com.appdeveloperblog.app.ws.service.impl;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.appdeveloperblog.app.ws.UserRepository;
import com.appdeveloperblog.app.ws.io.entity.UserEntity;
import com.appdeveloperblog.app.ws.service.UserService;
import com.appdeveloperblog.app.ws.shared.Utils;
import com.appdeveloperblog.app.ws.shared.dto.UserDto;

@Service
public class UserServiceImpl implements UserService {
	
	@Autowired
	UserRepository userRepository;
	
	@Autowired
	Utils utils;
	
	@Override
	public UserDto createUser(UserDto user) {
		
		if(userRepository.findByEmail(user.getEmail()) != null) throw new RuntimeException("Record already exist");
		
		
		UserEntity userEntity = new UserEntity();
		BeanUtils.copyProperties(user, userEntity);
		
		String publicUserId = utils.generateUserId(30);
		userEntity.setUserId(publicUserId);
		userEntity.setEncryptedPassword("test");
		
		UserEntity storedUserDetails = userRepository.save(userEntity);
		
		UserDto returnValue = new UserDto();
		BeanUtils.copyProperties(storedUserDetails, returnValue);
		
		
		return returnValue;
	}

}

여기서 UserEntity는 Entity클래스인데, 데이터베이스에 저장하기 위해 유저가 정의한 클래스가 필요한데 그런 클래스를 Entity라고 한다. Domain이라고 생각하면 된다. NodeJS로 비유해보자면 Sequelize에서 Model정의해 놓은 거라고 볼 수 있다. com.appdeveloperblog.app.ws.io.entity패키지에 정의했다.

package com.appdeveloperblog.app.ws.io.entity;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity(name="users")
public class UserEntity implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -3772691377276902875L;
	
	@Id
	@GeneratedValue
	private long id;
	
	@Column(nullable = false)
	private String userId;
	
	@Column(nullable = false, length=50)
	private String firstName;
	
	@Column(nullable = false, length=50)
	private String lastName;
	
	@Column(nullable = false, length=100)
	private String email;
	
	@Column(nullable = false)
	private String encryptedPassword;
	
	private String emailVerificationToken;
	
	@Column(nullable = false)
	private Boolean emailVerificationStatus = false;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getEncryptedPassword() {
		return encryptedPassword;
	}

	public void setEncryptedPassword(String encryptedPassword) {
		this.encryptedPassword = encryptedPassword;
	}

	public String getEmailVerificationToken() {
		return emailVerificationToken;
	}

	public void setEmailVerificationToken(String emailVerificationToken) {
		this.emailVerificationToken = emailVerificationToken;
	}

	public Boolean getEmailVerificationStatus() {
		return emailVerificationStatus;
	}

	public void setEmailVerificationStatus(Boolean emailVerificationStatus) {
		this.emailVerificationStatus = emailVerificationStatus;
	}
	
	
	
}

여기서 @Entity(name="users")로 정의해 놓은 부분은 DB에서 테이블 이름이다. 그리고 Entity클래스를 작성했다면, Repository인터페이스가 있어야 하는데, 스프링부트에서는 Entity의 기본적인 CRUD가 가능하도록 하는 Repository를 제공한다. Repository를 상속하기만 하면 된다.

package com.appdeveloperblog.app.ws;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.appdeveloperblog.app.ws.io.entity.UserEntity;

@Repository
public interface UserRepository extends CrudRepository<UserEntity, Long> {
	UserEntity findByEmail(String email);
}

JpaRepository를 상속받을 때는 사용될 Entity 클래스와 ID 값이 들어가게 된다.
즉, JpaRepository<T, ID> 가 된다.

그리고 또 여기서 신기한게 위의 기본기능을 제외한 조회 기능을 추가하고 싶으면 규칙에 맞는 메서드를 추가해주기만 하면 된다. 위에서 정의해 놓은 findByEmail을 작성해주기만 하면 Email을 가지고 SELECT하는 문장이 자동으로 실행되는 것이다. 물론 Entity에 email이라는 칼럼이 있어야 한다.
JPA 사용법 (JpaRepository) 이 블로그에 Entity, Repostory, findBy등 여러 유용한 정보가 있다.

다시 UserServiceImpl클래스로 돌아와서, createUser메서드를 보자.

if(userRepository.findByEmail(user.getEmail()) != null) throw new RuntimeException("Record already exist");

이부분에서 이메일을 찾고, 이메일이 존재한다면(중복된다면) 에러를 반환한다.

그리고 그 밑부분

UserEntity userEntity = new UserEntity();
		BeanUtils.copyProperties(user, userEntity);
		
		String publicUserId = utils.generateUserId(30);
		userEntity.setUserId(publicUserId);
		userEntity.setEncryptedPassword("test");
		
		UserEntity storedUserDetails = userRepository.save(userEntity);
		
		UserDto returnValue = new UserDto();
		BeanUtils.copyProperties(storedUserDetails, returnValue);
		
		
		return returnValue;

에서 아이디를 저장하고(userRepository.save(userEntity);) 그 결과를 반환한다. utils는 랜덤으로 userId를 생성해주는 메서드를 가지고 있는 클래스이다. com.appdeveloperblog.app.ws.shared패키지에 있다.

package com.appdeveloperblog.app.ws.shared;

import java.security.SecureRandom;
import java.util.Random;

import org.springframework.stereotype.Component;

@Component
public class Utils {
	
	private final Random RANDOM = new SecureRandom();
	private final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
//	private final int ITERATIONS = 10000;
//	private final int KEY_LENGTH = 256;

	public String generateUserId(int length) {
		return generateRandomString(length);
	}
	
	private String generateRandomString(int length) {
		StringBuilder returnValue = new StringBuilder(length);
		
		for(int i = 0; i < length; i++) {
			returnValue.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length())));
		}
		
		return new String(returnValue);
	}
}

이렇게 해서 userService에서 유저정보를 저장하고 그 결과가 userController클래스에서 createUser라는 userDto객체로 전달되고, 이게 다시 returnValue라는 UserRest객체로 전달되고 이 returnValue를 반환하게 된다.

여기까지 하면, 아직 할 게 많지만, 대략적인 흐름정도는 파악한 듯 싶다.

깃헙

주님 감사드립니다!

profile
임재현입니다.

0개의 댓글