반응형
반응형

📝프로젝트 구조 (Package structure)

Spring Boot에서 정해둔 프로젝트 구조는 없지만 저는 DDD가 유지보수에 더 좋다고 생각하여 DDD 기반으로 만들었습니다

 

DAO 기반의 DDD

com
  └ lsj
      └  shopping_mall
 	  	  └ dashboard
		  |	  └  controller
		  |	  └  service
		  |	  └  dao
		  |	  └  dto
		  |	      └ request
		  |	      └ response
		  |	  └  util
		  |	  └  exception
		  |	  
		  |
		  └ global
		        └ config
		        └ user
		        └ exception
		        └ jwt
		        └ util (공통 API)

 

Repository 기반의 DDD (+ JPA)

com
  └ lsj
      └  shopping_mall
 	  	  └ dashboard
		  |	  └  controller
		  |	  └  service
		  |	  └  repository
		  |	  └  entity
		  |	  └  dto
		  |	      └ request
		  |	      └ response
		  |	  └  util
		  |	  └  exception
		  |	  
		  |
		  └ global
		        └ config
		        └ user
		        └ exception
		        └ jwt
		        └ util (공통 API)

 

📝DAO vs Repository

DAO

// User.java
public class User {
    private Long id;
    private String name;
    private String email;

    // Constructors, getters and setters
}

// UserDao.java
public interface UserDao {
    User findById(Long id);
    List<User> findAll();
    void save(User user);
    void update(User user);
    void delete(Long id);
}

// UserDaoImpl.java
public class UserDaoImpl implements UserDao {
    private DataSource dataSource;

    public User findById(Long id) {
        // SQL 쿼리를 사용하여 특정 사용자 검색
    }

    public List<User> findAll() {
        // 모든 사용자를 검색하는 SQL 쿼리
    }

    public void save(User user) {
        // 사용자 저장 SQL 쿼리
    }

    public void update(User user) {
        // 사용자 정보 업데이트 SQL 쿼리
    }

    public void delete(Long id) {
        // 사용자 삭제 SQL 쿼리
    }
}

DAO는 데이터베이스와의 상호 작용을 캡슐화하여 데이터베이스 접근 로직과 비즈니스 로직을 분리합니다 특히 CRUD연산에만 초점을 맞춥니다 (DAO의 경우 데이터 액세스를 추상화)

 

위 코드를 CRUD로 구분하면 아래와 같습니다

  • Create
    • save
  • Read
    • findById
    • findAll
  • Update
    • update
  • Delete
    • delete

 

Repository

// UserRepository.java
public interface UserRepository {
    User findById(Long id);
    List<User> findAllByStatus(String status);
    void save(User user);
    void delete(User user);
}

// UserRepositoryImpl.java
public class UserRepositoryImpl implements UserRepository {
    private EntityManager entityManager;  // JPA 사용 예시

    public User findById(Long id) {
        return entityManager.find(User.class, id);
    }

    public List<User> findAllByStatus(String status) {
        return entityManager.createQuery("SELECT u FROM User u WHERE u.status = :status", User.class)
                            .setParameter("status", status)
                            .getResultList();
    }

    public void save(User user) {
        entityManager.persist(user);
    }

    public void delete(User user) {
        entityManager.remove(user);
    }
}

DAO와 다르게 도메인 중심의 설계에서 사용되며 CRUD에서 확장되어 각 상황에 맞는 도메인 로직에 더 초점을 맞춥니다

 

반응형
반응형
package com.lsj.blog;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.lsj.blog.repository.UserRepository;
import com.lsj.dto.ResponseDto;

@RestController
public class DummyController {

	@Autowired // DI
	private UserRepository userRepository;
	
	@PostMapping("/api/login")
	public ResponseDto<Integer> login(@RequestBody BlogUser user, HttpSession session){
		
		BlogUser principal = loginService(user); //principal (접근주체)
		if(principal != null) session.setAttribute("principal", principal);
		
		System.out.println("principal : " + session.getAttribute("principal"));
		
		return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
		
	}
	
	@Transactional(readOnly = true) // Select 할 때 트랜잭션 시작, 서비스 종료시 트랜잭션 종료 (데이터 일관성)
	public BlogUser loginService(BlogUser user) {
		return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword());
	}
}
package com.lsj.blog.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.lsj.blog.BlogUser;


// extends JapRepository가 있는 경우 @Repository 생략가능
// JpaRepostiory는 다양한 기능을 가지고 있다. findAll (모든 레코드 select) 페이징해서 보내줄 수 있는 기능도 존재
public interface UserRepository extends JpaRepository<BlogUser, Integer>{ // BlogUser의 Repository이고 PK는 Integer이다.
	
	// JPA Naming 쿼리
	// SELECT * FROM bloguser WHERE username = ? AND password = ?;
	// SELECT * FROM ${리턴타입} WHERE username = ${첫번째 파라미터} AND password = ${두번째 파라미터};
	BlogUser findByUsernameAndPassword(String username, String password);
	
	// nativeQuery 방식 위와 동일하다
	@Query(value="SELECT * FROM bloguser WHERE username =1 ? AND password = 2?", nativeQuery = true)
	BlogUser login(String username, String password);
}
반응형
반응형

오라클의 경우 Read Commit 방식을 채택한다.

트랜잭션A가 시작되어 Update를 할 경우 Commit이 이루어질 때까지 UnDo의 영역을 읽기 때문에 Update하고 Commit의 사이에서 누군가 select할 시 1,000원이 출력된다.

 

트랜잭션A가 진행되면 누군가한테는 홍길동 누군가한테는 1,000원 누군가한테는 2,000원이 보일 수 있다.

이런식으로 보일 수 있기 때문에 정합성이 깨진다 → Phantom Read (데이터가 보였다 안 보였다)

 

이러한 점의 문제점은 트랜잭션 B와 같이 조회한 4개를 더해서 총 합을 구하는 시스템이라면 도중에 원래 기대값이 4,000원이지만 6,000원이 나오는 문제가 생긴다.

 

 

MySQL의 경우 Repeatable Read 방식을 채용한다.

UnDo 영역에 데이터를 읽고 자기 트랜잭션 번호보다 낮은 데이터를 읽기 때문에 중간에 Update를 해도 데이터의 일관성을 지킬 수 있다.

 

Insert, Select, Update, Delete의 경우 모두 @Transactional 이 필요하다

반응형
반응형
package com.lsj.blog;

import java.util.Random;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;
import com.lsj.dto.ResponseDto;

@RestController
public class DummyController {

	@Autowired // DI
	private UserRepository userRepository;
	
	@PostMapping("/api/user")
	@Transactional
	public ResponseDto<String> save(@RequestBody BlogUser user){

		BlogUser usre2 = new BlogUser();
		
		Random random = new Random();

		usre2.setUsername(String.valueOf(random.nextInt(10)));
		usre2.setPassword("12345"); 
		usre2.setEmail("abc@naver.com");
		userRepository.save(usre2); 
			
		new ResponseDto<String>(HttpStatus.OK.value(), "데이터 insert에 성공했습니다.");
		// HttpStatus.OK.value() → 200이라는 코드 반환 (정상 성공)
	}
}
package com.lsj.blog.handler;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import com.lsj.dto.ResponseDto;

@ControllerAdvice // 어떤 Exception이 오든 이 페이지로 오게 된다.
@RestController
public class GlobalExceptionHandler {
	
	@ExceptionHandler(value=Exception.class) // 모든 부모 Exception
	public ResponseDto<String> handleException(Exception e) {
		return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(),e.getMessage());
		// HttpStatus.INTERNAL_SERVER_ERROR.value() → 500 코드 반환 (서버 에러)
		//return "<h1>" + e.getMessage() + "</h1>";
	}
}
package com.lsj.dto;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
	int status;
//	HttpStatus status;
	T data;
}

 

반응형
반응형
package com.lsj.blog.handler; // Exception 처리 Handler를 따로 패키지를 구성한다.

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

@ControllerAdvice // 어떤 Exception이 오든 이 페이지로 오게 된다.
@RestController
public class GlobalExceptionHandler {
	
	// @ExceptionHandler는 Controller계층에서 발생하는 에러만 잡는다.
	@ExceptionHandler(value=Exception.class) // 모든 부모 Exception 
	public String handleException(Exception e) {
		return "<h1>" + e.getMessage() + "</h1>";
	}
	
	@ExceptionHandler(value=IllegalArgumentException.class) // 해당 IllegalArgumentException이 발생할 경우 밑에 함수가 작동한다.
	public String handleArgumentException(IllegalArgumentException e) {
		return "<h1>" + e.getMessage() + "</h1>";
	}
}
반응형
반응형
@DeleteMapping("dummy/user/{id}")
public String deleteUser(@PathVariable int id) {

    /** 방법1 (예외처리가 안 되어있음)**/
    //userRepository.deleteById(id);

     /** 방법2 **/
    try {
        userRepository.deleteById(id);
    }catch (Exception e) {
        return "삭제에 실패했습니다. 해당 id는 존재하지 않습니다.";
    }

    return "삭제되었습니다.";
}
반응형
반응형

 

영속성 컨텐스트란 엔티티를 영구 저장하는 환경으로 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스라고 생각하면 된다.

JPA는 영속성 컨텍스트라는게 존재하고 그 안에 캐시가 있어서 DB에 접근 안 하고 캐시에 존재하는 데이터는 빠르게 가져오거나 DB에 부담을 줄일 수 있습니다. 그리고 그 캐시안에 있는 걸 영속화 되었다라고 표현합니다.

 

📝Insert

Insert시 영속성 컨텍스트에 해당 값이 올라가게 되고 flush해 영속성 컨텍스트에 있는 데이터를 DB에 데이터를 넣게 됩니다.

 

📝Select

Select시 DB에 해당 데이터를 영속성 컨텍스트에 올리게 됩니다. 그 후 컨트롤러에서는 영속성 컨텍스트에 있는 값을 가져가게 됩니다.

 

📝Update

Update의 경우 Select와 같은 동작이 선행이 되어서 영속성 컨텍스트에 올라가게 되고 그 후 업데이트된 값을 다시 영속화 시키고 DB에 flush해 데이터를 업데이트 시킵니다.

 

📝더티체킹

Update와 같은 행위를 Transaction 어노테이션을 이용해 하는 걸 더티체킹이라고 합니다.

Transaction 어노테이션이 선언된 곳에서 영속성 컨텍스트에 변경이 일어날 시 자동으로 데이터베이스에 반영하는 JPA 특징입니다.

 

 

@Transactional // 함수 종료시 자동으로 DB Commit (영속성컨테이너 데이터를 DB로 Flush)
// Update ( Put방식은 Body에 Json만 인식하고 @RequestBody는 두개 이상 사용이 불가능 (객체에 담던가 해야한다.)
@PutMapping("/dummy/users/{id}") 
public String updateUser(@PathVariable int id, @RequestBody BlogUser requestUser) {
    System.out.println("id : " + id);
    System.out.println("password : " + requestUser.getPassword());
    System.out.println("email : " + requestUser.getEmail());

    BlogUser user = userRepository.findById(id).orElseThrow(() ->{
        return new IllegalArgumentException("수정에 실패하였습니다.");
    });

    user.setPassword(requestUser.getPassword());
    user.setEmail(requestUser.getEmail());

    /** @Transactional 사용 안 할시 **/
    // save 함수는 id를 전달하지 않으면 insert
    // save 함수는 id를 전달하면 해당 id 존재시 udpate 없으면 insert
    // userRepository.save(user);

    return id + "가 update 가 되었습니다. ";
}

 

 

@Transaction 다른 예제

@RestController
public class DummyController {

	@Autowired // DI
	private UserRepository userRepository;
	
	@PostMapping("/api/user")
	//@Transacitonal
	public ResponseDto<String> save(@RequestBody BlogUser user){

		BlogUser usre2 = new BlogUser();
		
		Random random = new Random();

		usre2.setUsername(String.valueOf(random.nextInt(10)));
		usre2.setPassword("abc@naver.com");
		usre2.setEmail("abc@naver.com");
		userRepository.save(usre2); 
		
		user.setRole(RoleType.USER); // Enum을 이용한 방식을 통해 Setter 주입 실수를 방지할 수 있다. 
		join(user);
	
		return new ResponseDto<String>(HttpStatus.OK .value(), "1");
		// new ResponseDto<String>(HttpStatus.OK, result);
	}
	
	@Transactional // Transaction 처리하겠다. 성공시에만 Commit 
	public void join(BlogUser user) { 
		userRepository.save(user); // UserRepostiory에 save하면 DB 테이블에 insert가 된다.
	}
	
 }
 
 
 데이터 Body 형식 →
 {
    "username" : "lee6",
    "password" : "1234",
    "email" : "tjdwo37@naver.com"
}
 
 
 
 - @Transaction이 join 함수에 있는 경우
join 함수를 실행시에 에러가 날시 join함수 내용만 rollback
위에 랜덤함수로 UserName을 넣고 insert 과정은 join함수를 안 쓰기 때문에 
해당 API 계속 호출시 랜덤함수로 UserName 넣는 데이터 계속 insert는 되고 있음

- @Transcaation을 api 함수에 넣은 경우
api에서 에러가 날시 모든 내용을 rollback하기 때문에 위와 같이 랜덤함수로 UserName넣고 insert하는 과정도
rollback이 되어서 데이터가 계속 누적되지 않는다.
package com.lsj.blog;

import java.sql.Timestamp;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;

import com.lsj.blog.model.RoleType;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/** 하기 어노테이션 사용하려면 Javax Persistence API Gradle 추가 필요 **/
//@DynamicInsert // null 인 값일 때는 무시하고 Default값으로 넣어준다
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity // 하기 필드 기준으로 DB에  User 테이블을 생성하라는 의미
public class BlogUser {

	@Id // Primary key
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
	// → 즉, DB에 따라 AutoIncrement 또는 Sequence가 결정 되어서 알아서 만들어준다. (자동 증가 PK 필요함으로 이와 같은 행동 함) 
	// (MySql에서는 AutoIncrement, Oracle에서는 Sequence 사용)
	private int id;
	
	@Column(nullable = false, length = 30, unique=true) // null 방지 및 길이 30 + 중복값 방지
	private String username;
	
	//@ColumnDefault(" 'no password' ") //→ @DynamicInsert 와 같이 사용한다. null일시에는 ColumnDefault에 선언된 값(no password)으로 넣어준다.   
	@Column(nullable = false, length = 100) 
	private String password;

	@Column(nullable = false, length = 50) 
	private String email;
	
	//@ColumnDefault("'user'") // → Column Default 값 String이기 때문에 ' ' 안에 넣어야한다.
	//private String role; // Enum을 
	
	@Enumerated(EnumType.STRING) // DB에는 RoleType이라는 타입이 없기 때문에 String 이라고 선언
	private RoleType role;
	
	@CreationTimestamp // 현재 시각 바로 자동 입력
	private Timestamp createDate;
	
}

 

반응형
반응형
import java.util.List;
import java.util.function.Supplier;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;


@GetMapping("/dummy/user/{id}")
public BlogUser detail(@PathVariable int id) { // PathVariable은 들어온 id값에 따른 동적 처리 {id}


    // BlogUser user = userRepository.findById(id); // findById의 return 타입은 Optional
                                                    // → 자바에서 DB 데이터를 못찾아오게 되면 user가 null이 되어 return이 되기 때문에
                                                    // Optional로 객체를 감싸서 가져올테니 null인지 아닌지 판단해서 return 해라
    /** 방법1 **/
    // userRepository.findById(id).get(); // null 그런거 상관없이 가져오겠다. (위험) 

    /**  방법 2
    BlogUser user = userRepository.findById(id).orElseGet(new Supplier<BlogUser>(){
        @Override
        public BlogUser get() { // 값이 없는 경우 BlogUser 객체 생성해 주입 (Null 가능성은 없음)
            return new BlogUser();
        }
    });
    **/

    /** 방법3 (선호) 해당 에러 발생시 Throw **/
    BlogUser user = userRepository.findById(id).orElseThrow(new Supplier<IllegalArgumentException>(){
        @Override
        public IllegalArgumentException get() {
            return new IllegalArgumentException("해당 유저는 없습니다. id : " + id);
        }
    });

    return user;
}

// 페이지리스트 모두 출력
@GetMapping("/dummy/list")
public List<BlogUser> list(){
    return userRepository.findAll();
}

// 페이지네이션
// size → 보여줄 개수
// sort → order by 할 필드
// direction  → 오름차순 또는 내림차순
// dummy/list/page?page=0 일시 id 3번, 2번 노출  | page=1로 호출시 id 1번 노출
// 아무것도 입력안 할시 ?page=0과 동일
@GetMapping("dummy/list/page")
public Page<BlogUser> pageList(@PageableDefault(size=2, sort="id", direction = Sort.Direction.DESC) Pageable pageable){
    Page<BlogUser> users = userRepository.findAll(pageable);
    return users;
}

 

반응형
반응형
package com.lsj.blog;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.lsj.blog.model.RoleType;
import com.lsj.blog.repository.UserRepository;

@RestController
public class DummyController {

	@Autowired // DI
	private UserRepository userRepository;
	
	@PostMapping("/dummy/join")
	public String join(BlogUser user) { 
		
		//user.setRole("user"); // @DynamicInsert처럼 계속 어노테이션을 붙히다보면 어노테이션 지옥에 빠지기 때문에 직접 Setter에 주입해 어노테이션을 줄일 수 있지만 실수할 수 있다.
		user.setRole(RoleType.USER); // Enum을 이용한 방식을 통해 Setter 주입 실수를 방지할 수 있다. 
		userRepository.save(user); // UserRepostiory에 save하면 DB 테이블에 insert가 된다.
		
		return "회원가입이 완료되었습니다.";
	}
}

DummyController.java

 

package com.lsj.blog.model;

//데이터 강제할 수 있다. (데이터의 도메인화 → 강제)
public enum RoleType {
	USER, 
	ADMIN 
}

RoleType.java

 

package com.lsj.blog.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.lsj.blog.BlogUser;

// extends JapRepository가 있는 경우 @Repository 생략가능
// JpaRepostiory는 다양한 기능을 가지고 있다. findAll (모든 레코드 select) 페이징해서 보내줄 수 있는 기능도 존재
public interface UserRepository extends JpaRepository<BlogUser, Integer>{ // BlogUser의 Repository이고 PK는 Integer이다.
	
}

UserRepository.java

 

package com.lsj.blog;

import java.sql.Timestamp;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;

import com.lsj.blog.model.RoleType;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/** 하기 어노테이션 사용하려면 Javax Persistence API Gradle 추가 필요 **/
@DynamicInsert // null 인 값일 때는 무시하고 Default값으로 넣어준다
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity // 하기 필드 기준으로 DB에  User 테이블을 생성하라는 의미
public class BlogUser {

	@Id // Primary key
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
	// → 즉, DB에 따라 AutoIncrement 또는 Sequence가 결정 되어서 알아서 만들어준다. (자동 증가 PK 필요함으로 이와 같은 행동 함) 
	// (MySql에서는 AutoIncrement, Oracle에서는 Sequence 사용)
	private int id;
	
	@Column(nullable = false, length = 30, unique=true) // null 방지 및 길이 30 + 중복값 방지
	private String username;
	
	@ColumnDefault(" 'no password' ") //→ @DynamicInsert 와 같이 사용한다. null일시에는 ColumnDefault에 선언된 값(no password)으로 넣어준다.   
	@Column(nullable = true, length = 100) 
	private String password;

	@Column(nullable = false, length = 50) 
	private String email;
	
	//@ColumnDefault("'user'") // → Column Default 값 String이기 때문에 ' ' 안에 넣어야한다.
	//private String role; // Enum을 
	
	@Enumerated(EnumType.STRING) // DB에는 RoleType이라는 타입이 없기 때문에 String 이라고 선언
	private RoleType role;
	
	@CreationTimestamp // 현재 시각 바로 자동 입력
	private Timestamp createDate;
	
}


/**
위와 같은 엔터티일 때 세이브할 데이터 형식 예제
{
    "username" : "lee6",
    "email" : "abc@naver.com"
}

결과 → 2	22/12/03 12:31:26.986000000	tjdwo37@naver.com	no password	USER	lee6
**/

BlogUser.java

반응형