본문 바로가기

내일배움캠프

Spring 입문주차 2주차

1. 3 Layer Architecture

 다음은 어느 프로젝트의 컨트롤러 클래스 일부이다.

@RestController
@RequestMapping ("/api")
public class MemoController {
    private final Map<Long, Memo> memoList = new HashMap<>();
    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);

        // Memo Max ID check
        Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
        memo.setId(maxId);
        // DB 저장
        memoList.put(memo.getId(), memo);

        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
        return memoResponseDto;
    }
    @GetMapping("/memos")
    public List<MemoResponseDto> getMemos() {
        // Map to List
        List<MemoResponseDto> responseList = memoList.values().stream()
                .map(MemoResponseDto::new).toList();
        return responseList;
    }
}

이렇게 컨트롤러 클래스 하나에 api가 처리하는 모든 과정을 담아놓으면 개발속도는 빠르겠지만 코드가 비대해 질 수록 유지보수가 어려울 것이고 코드 분석이 복잡해서 다른 사람에게 인수인계 하기에도 어려울 것이다.

Spring에서는 이러한 문제점들을 해결하기 위해 서버 개발자들은 서버에서의 처리과정이 대부분 비슷하다는 걸 이용해 처리 과정을 크게 Controller, Service, Repository 3개로 분리했다.

Controller는 클라이언트의 요청을 받고 요청에 대한 로직처리를 Service에 넘긴다. 물론 request데이터가 필요하면 같이 넘긴다. 그리고 처리 결과를 반환하는 역할을 한다.

Service는 클라이언트의 요청에 대한 로직 처리를 담당한다. DB저장 및 조회가 필요하면 Repository를 호출한다.

Repository는 DB CRUD작업을 한다.

 

3 Layer Architecture 예시:

public class MemoService { 
    private final JdbcTemplate jdbcTemplate;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    	// 새 메모를 저장하는 실제 로직을 수행
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        // DB 저장
        Memo saveMemo = memoRepository.save(memo);
        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);

        return memoResponseDto;
    }
public class MemoRepository {
    private final JdbcTemplate jdbcTemplate;
    public MemoRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public Memo save(Memo memo) {
    	// 클라이언트가 입력한 메모 db에 저장
        KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체

        String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
        jdbcTemplate.update( con -> {
                    PreparedStatement preparedStatement = con.prepareStatement(sql,
                            Statement.RETURN_GENERATED_KEYS);

                    preparedStatement.setString(1, memo.getUsername());
                    preparedStatement.setString(2, memo.getContents());
                    return preparedStatement;
                },
                keyHolder);

        // DB Insert 후 받아온 기본키 확인
        Long id = keyHolder.getKey().longValue();
        memo.setId(id);
        return memo;
    }
@RestController
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    	// 기존의 컨트롤러는 request를 받고 response를 반환하는 역할만 한다.
        MemoService memoService = new MemoService(jdbcTemplate);
        return memoService.createMemo(requestDto);
    }

 

2. 의존성 주입과 약한결합

위의 코드를 보면 api가 호출 될 때마다 매번 새로운 service객체가 생성되고 새로운 repository객체가 생성된다. 새로운 객체를 이렇게 자주 생성하면 리소스 낭비이다. 객체를 한번만 생성하고 같은 역할의 객체는 재사용 하는 코드가 좋은 코드이다.

@RequestMapping("/api")
public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.memoService = new MemoService(jdbcTemplate);
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    	//서비스 객체는 서버가 실행될 때 한번만 생성되고 api가 호출할 때는 생성된 객체를 재사용한다.
        return memoService.createMemo(requestDto);
    }
public class MemoService {
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    	// Repository객체도 한번만 생성하고 재사용한다.
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);
        // DB 저장
        Memo saveMemo = memoRepository.save(memo);
        // Entity -> ResponseDto
	}

 

그리고 service와 repository의 생성자를 보면 모두 JdbcTemplate를 추가하고 있는데 JDBC기능을 사용하기 위해 실질적으로 JDBC와 연관이 있는 클래스는 repository하나인데도 불구하고 모든 클래스에 JdbcTemplate을 선언하고 있다. 만약 JdbcTemplate이 아닌 다른 객체를 넣겠다고 하면 모든 클래스를 찾아서 수정해야 한다. 다시 말해 JdbcTemplate은 다른 클래스들과 '강한 결합'이 되어 있다. 

이걸 해결하기 위해 미리 만들어둔 repository를 service에 주입하고, 또 이렇게 만들어진 service를 controller에 주입하는 방법으로 해결할 수 있다.

@RequestMapping("/api")
public class MemoController {

    private final MemoService memoService;

    public MemoController(JdbcTemplate jdbcTemplate) {
        this.memoService = memoService;
    }

    @PostMapping("/memos")
    public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    	//서비스 객체는 서버가 실행될 때 한번만 생성되고 api가 호출할 때는 생성된 객체를 재사용한다.
        return memoService.createMemo(requestDto);
    }
public class MemoService {
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = memoRepository;
    }
    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    	// Repository객체도 한번만 생성하고 재사용한다.
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);
        // DB 저장
        Memo saveMemo = memoRepository.save(memo);
        // Entity -> ResponseDto
	}

여기서 의문점이 생긴다. 미리 만들어둔 객체를 주입하려면 생성을 해야 하는데 이건 어디서 생성하는가? 그건 Spring이 필요한 객체를 생성해 준다.

 

3. Bean

앞에서 Spring에서는 필요한 객체를 생성해 준다고 했다. 이런 객체들을 Bean이라고 하고 Bean을 모아둔 컨테이너를 IoC 컨테이너라고 한다.

클래스를 Bean에 등록하려면 @Component 애너테이션을 사용하면 된다.

@Component
public class MemoService { ... }

여러개의 클래스를 한번에 Bean에 등록하려면 @ComponentScan 애너테이션을 사용하면 된다. 패키지 안에있는 모든 클래스를 Bean에 등록한다.

@Configuration
@ComponentScan(basePackages = "com.sparta.memo")
class BeanConfig { ... }

Bean을 사용하려면 주로 클래스의 생성자 위에 @Autowired 애너테이션을 붙여준다.

@Component
public class MemoService {
		
    @Autowired
    private MemoRepository memoRepository;
		//memoRepository ‘Bean’을 해당 필드에 DI(Dependency Insertion) 즉, 의존성을 주입한다.
		// ...
}

물론 그 외 setter계열의 메소드에도 붙일 수는 있다.

@Autowired는 생성자가 한 개일 경우 생략 가능하다.

@Component
@RequiredArgsConstructor // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}

참고로 lombok의 @RequiredArgsConstructor을 이용하면 생성자도 생략가능하다.

 

Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용한다. 컨트롤러는 @Controller 혹은 @RestController 서비스는 @Service, 레포지토리는 @Repository로 명시한다. 다시 말해 @Component을 대체하는 용도로 사용할 수 있다.

 

4. JPA core

JPA는 Java Persistence API의 약자로 Java ORM 기술의 대표적인 표준 명세를 뜻한다.

ORM이란 Object-Relational Mapping의 약자로 객체-관계(지향형 db) 매핑의 약자로 DB와 자바의 객체를 연결해 주는 도구이다.

JPA는 애플리케이션과 JDBC 사이에서 동작한다. JPA는 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 쉽게 DB 작업을 처리할 수 있습니다. 예를 들면 JDBC로 insert sql문을 문자열로 저장하고 실행시키는 과정을 save()메소드(예시이다.) 하나로 처리할 수 있다.

5. Entity

Entity는 JPA에서 관리되는 객체로 DB의 테이블과 매핑되어 JPA에 의해 관리된다.

Entity 예시

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정, 매개변수로 Entity의 이름을 정할 수 있다. 디폴트 네임: 현재 클래스명
@Table(name = "memo") // 매핑할 테이블의 이름을 지정, 디폴트 네임: Entity 클래스 이름
public class Memo {
    @Id
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용), 디폴트 값:false
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 길이 지정, 디폴트 값:255
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}

@GeneratedValue 옵션으로 기본 키 생성을 DB에 맡겨 자동으로 처리하게 할 수 있다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키의 조건에 auto_increment를 적용된다.
private Long id;

 

6. 영속성 컨텍스트

Entity를 효율적으로 관리하기 위해 만들어진 공간이다. 왜 영속성이냐면 entity가 객체의 생명이나 공간을 자유롭게 유지할 수 있기 때문이다. 데이터베이스의 특성 ACID중 D(Durablity, 영속성)과 연관이 있다.

 

영속성 컨텍스트에 접근해서 entity 객체를 조작하려면 EntityManager가 필요하다. 개발자들은 EntityManager로 Entity를 조작할 수 있다. EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있다.

 

EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용된다.

본래 EntityManagerFactory를 만들기 위해서는 persistence.xml 파일을 만들어 전달해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="memo">
        <class>com.sparta.entity.Memo</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>

            <property name="hibernate.hbm2ddl.auto" value="create" />

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

하지만 SpringBoot에서는 이런 복잡한 파일을 수동으로 만들지 않아도 EntityManagerFactory와 EntityManager를 자동으로 생성해줍니다. application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성된다.

 

7. Spring Data JPA

Spring Data JPAJPA를 쉽게 사용할 수 있게 만들어놓은 모듈이다. JPA를 추상화시킨 Repository 인터페이스를 제공한다. Repository 인터페이스 Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용한다.

(Hibernate는 JPA를 구현한 프레임워크중 하나이다. JPA를 구현한 프레임워크중 실질적인 표준이다.)

 

Spring Data JPA를 사용하려면 Repository를 JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 인터페이스로 선언하면 된다.

예시:

public interface MemoRepository extends JpaRepository<Memo, Long> {

}

 

jdbc에서 insert문을 쓰고 싶다면 jpa에선 save()메소드를 쓰면 된다.

예시:

// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

return memoResponseDto;

 

테이블의 모든 데이터를 조회하고 싶으면 findAll()메소드를 사용하면 된다.

예시:

public List<MemoResponseDto> getMemos() {
    return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}

예시에서는 MemoResponse객체로 변환하기 위해 stream객체의 map()메소드를 사용했다.

 

특정 id값을 가진 테이블의 데이터만 조회하고 싶으면 findById()메소드를 사용한다.

 

예시:

private Memo findMemo(Long id) {
    return memoRepository.findById(id).orElseThrow(() ->
            new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
    );
}

코드 원문:

public Optional<T> findById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    Class<T> domainType = this.getDomainClass();
    if (this.metadata == null) {
        return Optional.ofNullable(this.entityManager.find(domainType, id));
    } else {
        LockModeType type = this.metadata.getLockModeType();
        Map<String, Object> hints = this.getHints();
        return Optional.ofNullable(type == null ? this.entityManager.find(domainType, id, hints) : this.entityManager.find(domainType, id, type, hints));
    }
}

findById의 원문을 보면 옵셔널 객체로 리턴한다. 그래서 findById를 Spring에서 활용하기 위해서는 orElse()메소드로 언래핑하는 작업이 필요하다. 예시에서는 orElseThrow()라는 null일 경우 예외를 발생시키는 메소드로 언래핑했다.

 

테이블에서 특정 레코드를 삭제하고 싶으면 delete()메소드를 사용한다.

예시:

public Long deleteMemo(Long id) {
    // 해당 메모가 DB에 존재하는지 확인
    Memo memo = findMemo(id);
    // memo 삭제
    memoRepository.delete(memo);
    return id;
}

 

jpa에는 update메소드가 따로 없다. jpa는 영속성 컨텍스트를 사용한다. 영속성 컨텍스트에는 변경감지 기능이라고 쉽게 얘기해 Entity객체의 값이 변경되면 db에 바로 값을 반영하는 기능이 있다. update기능은 이런 변경감지 기능을 이용해 구현해야 한다.

영속성 컨텍스트의 기능을 이용하려면 @Transactional애너테이션을 추가해야 한다.

예시:

@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    // 해당 메모가 DB에 존재하는지 확인
    Memo memo = findMemo(id);
    // memo 내용 수정
    memo.update(requestDto);
    return id;
}
public void update(MemoRequestDto requestDto) {
    this.username = requestDto.getUsername();
    this.contents = requestDto.getContents();
}

 

8. JPA Auditing

Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공한다.

JPA Auditing을 사용하려면 @EnableJpaAuditing을 main클래스에 붙여줘야 한다.

@EnableJpaAuditing
@SpringBootApplication
public class MemoApplication {
...

 

JPA Auditing 예시:

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}

@MappedSuperclass가 있으면 추상 클래스인 Timestamped클래스를 상속받은 Entity는 createdAt, modifiedAt을 컬럼으로 인식할 수 있다.

@EntityListeners(AuditingEntityListener.class) 해당 클래스에 Auditing기능을 포함시킨다.

@CreatedDate는 Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장된다. 최초 생성시간은 수정되면 안되기 때문에 (updatable = false)를 추가한다.

@Temporal는 날짜 타입을 db의 자료형과 매핑할 때 사용한다. 날짜 타입에는 DATE(날짜), TIME(시간), TIMESTAMP(날짜와 시간)이 있다.

 

9. Query Methods

Query Methods는 Spring Data JPA에 정의되지 않은 복잡한 쿼리를 메소드 이름으로 사용할 수 있게 해주는 기능이다.

예를 들면 id에 맞는 로우를 찾아주는 findById 대신 이름에 맞는 로우를 찾아주는 쿼리를 쓰거나, 기존 메소드에 없는 orderby기능을 사용할 수 있다.

Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면 자동으로 구현해준다. 별도로 구현하는 코드를 쓸 필요는 없다.

예시:

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc();
}
public List<MemoResponseDto> getMemos() {
    return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}

이렇게 선언만 해도 알아서 메소드를 생성해주기 때문에 사용 가능하다.

 

'내일배움캠프' 카테고리의 다른 글

Spring 숙련주차 Part2  (0) 2024.05.24
Spring 숙련주차 Part1  (0) 2024.05.22
Spring 입문주차 1주차  (0) 2024.05.17
Spring 일정관리 앱 프로젝트 2일차  (0) 2024.05.16
Spring 일정관리 앱 프로젝트 1일차  (0) 2024.05.14