Sad Puppy 3 '프레임워크/Spring' 카테고리의 글 목록 :: 개발자 아지트

[Java-based Configuration]

: Java 코드와 스프링이 제공하는 어노테이션을 통해 스프링 컨테이너를 정의하는 방법 학습

 

[Declaring a Bean]

:@Configuration  어노테이션이 달린 클래스와 @Bean 어노테이션이 달린 메서드를 통해 Java 코드에서 스프링 빈을 등록할 수 있다. 

 

빈으로 등록하고자 하는 함수의 리턴값에서 실제 구현체가 없으면 빈이 등록되지 않는다. 

 

 

 

[Bean Dependencies]

:@Configuration 어노테이션이 있는 클래스 내의 메서드는 같은 클래스 내의 다른 @Bean 메서드를 호출해 빈 간의 의존성을 정의할 수 있다. 

 

@Configuration
@ComponentScan(basePackages = "cholog.scan")
public class AppConfig {

    @Bean
    public AuthService authService() {
        // 실제 구현체가 없으면 빈이 등록되지 않는다.
        return new AuthService();
    }
    
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver(AuthService authService) {

        return new AuthenticationPrincipalArgumentResolver(authService);
    }

}

 


[Property]

: 이는 애플리케이션의 구성값을 key-value 쌍으로 저장한다. 데이터베이스 연결 정보나 API 키 값을 예를 들 수 있다. 

스프링의 Environment 인터페이스는 이와 같은 프로퍼티 소스들을 통합해 관리하고, 필요한 프로퍼티 값을 조회하는 기능을 제공한다. 

 

[Using @PropertySource and Environment]

: @PropertySource 어노테이션을 사용해 프로퍼티 파일을 로드하고, Environment를 사용해 프로퍼티 값을 읽어올 수 있다. 

 

 

// TODO: Java-based Configuration을 하기 위한 클래스로 지정하기
// TODO: ext-api.properties 파일을 활용하기 위한 설정 추가하기
@Configuration
@PropertySource("classpath:ext-api.properties")
public class PropertySourceConfig {

    private final Environment env;

    public PropertySourceConfig(Environment env) {
        this.env = env;
    }

    // TODO: ext-api.properties의 google.api.endpoint 값을 Environment를 사용해서 가져오기
    // TODO: 위 endpoint 값을 사용하여 GoogleMapsRestClient를 빈으로 등록하기
    @Bean
    public GoogleMapsRestClient googleMapsRestClient() {
        String endpoint = env.getProperty("google.api.endpoint");
        return new GoogleMapsRestClient(endpoint);
    }

    // TODO: ext-api.properties의 google.api.endpoint 값을 어노테이션을 사용해서 가져오기
    // TODO: 위 endpoint 값을 사용하여 GoogleMapsRestClient를 빈으로 등록하기
    public GoogleDriveRestClient googleDriveRestClient() {
        return new GoogleDriveRestClient("");
    }
}

 

이해가 잘 안된다..

 

[Using @PropertySource and @Value]

: @PropertySource를 사용해 로드한 프로퍼티 파일의 값을 @Value 어노테이션을 통해 주입할 수 있다. 

스프링에서 @Value 어노테이션을 사용하면 ext-api.properties 파일에 있는 값을 필드나 메서드 파라미터에 주입할 수 있다. 

 

 

package cholog.property.config;

import cholog.property.GoogleDriveRestClient;
import cholog.property.GoogleMapsRestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

@Configuration
@PropertySource("classpath:ext-api.properties")
public class PropertySourceConfig {

    private final Environment env;

    public PropertySourceConfig(Environment env) {
        this.env = env;
    }

    @Bean
    public GoogleMapsRestClient googleMapsRestClient() {
        String endpoint = env.getProperty("google.api.endpoint");
        return new GoogleMapsRestClient(endpoint);
    }
    
    @Value("${google.api.endpoint}")
    private String googleApiEndpoint;

    @Bean
    public GoogleDriveRestClient googleDriveRestClient() {
        return new GoogleDriveRestClient(googleApiEndpoint);
    }
}

 

[Externalized Configuration (Spring Boot)]

: 프로퍼티 값을 설정하는 방법은 @PropertySource 외에도 다양하다. 특히 Spring Boot 를 사용한다면 application.properties(혹은 application.yaml) 파일을 사용해 프로퍼티 값을 편하게 설정할 수 있다. 

 

package cholog.property.config;

import cholog.property.JwtTokenKeyProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// TODO: Java-based Configuration을 하기 위한 클래스로 지정하기
@Configuration
public class AuthConfig {
    // TODO: application.properties의 security.jwt.token.secret-key 값을 활용하여 JwtTokenKeyProvider를 빈으로 등록하기

    @Value("${security.jwt.token.secret-key}")
    private String secretKey;

    @Bean
    public JwtTokenKeyProvider jwtTokenKeyProvider() {
        return new JwtTokenKeyProvider(secretKey);
    }
}

 


[Profile]

: 프로파일은 애플리케이션 설정의 논리적인 그룹이다. 예를 들어, 개발(development), 테스트(testing), 운영(production)과 같이 다른 환경에 적합한 설정을 분리할 수 있다.

프로파일을 사용하면 같은 애플리케이션이지만 환경에 따라 다른 구성을 적용할 수 있다.

스프링이 제공하는 Environment 인터페이스는 현재 활성화되어 있는 프로파일과 기본 프로파일을 관리한다. 

 

[@Profile]

: @Profile 어노테이션을 이용하여 특정 프로파일에 따라 빈을 등록할 수 있다. @Profile 어노테이션은 클래스 레벨, 메서드 레벨에 모두 적용 가능하다. 

 

@Configuration 클래스에 적용하는 경우

 

클래스 내에서 정의된 Bean들은 development profile일 때만 등록된다. 

 

@Bean 메서드에 적용하는 경우

 

profile에 따라 등록되는 Datasource Bean이 달라집니다.

 

@Configuration
public class ProfileConfig {

    @Bean("dataSource")
    @Profile("dev")
    public MessageRepository inMemoryMessageRepository() {
        return new InmemoryMessageRepository();
    }

    @Bean("dataSource")
    @Profile("prod")
    public MessageRepository jdbcMessageRepository() {
        return new JdbcMessageRepository();
    }

}

 

 

 

스프링 컨테이너를 정의하는 방법은 다양합니다.@Component, @Autowired 어노테이션을 사용하는 방법과 비교하여 Java 코드로 빈을 관리할 때의 장단점에 대해 생각해보고, 어떤 상황에서 어떤 방식을 택할지 고민해보세요.이 외에도 XML을 사용해 스프링 컨테이너를 정의할 수도 있습니다. XML을 사용하는 방법에 대해 알아보고, Java 코드와 XML을 사용하는 방법을 비교해보세요.Spring Boot는 프로퍼티 파일 컨벤션(application-{profile})을 사용해 활성 프로파일에 대한 프로퍼티 파일을 로드합니다. 예를 들어, 활성 프로파일이 prod 라면 application.properties, application-prod.properties 파일을 로드합니다. 이러한 특성을 사용해 어떤 값을 프로퍼티 파일에서 관리할지 생각해 보세요.

 

[Spring Bean]

: 스프링은 애플리케이션의 복잡성을 줄이고, 유지보수를 용이하게 하기 위해서 객체의 생성, 설정 및 생명주기를 관리하는 스프링 컨테이너를 제공한다. 해당 컨테이너가 관리하는 객체를 스프링 빈이라고하고, 이를 통해서 의존성 주입이나 객체 관리가 자동화된다. 

 

[Bean Registration]

: 스프링에서 빈을 등록하는 방법은 다양한다. (= 빈을 만드는 방법)

 

어노테이션을 사용해 클래스에 추가하는 방법이 있다. 

 

@Component

: 스프링아, 이 클래스를 빈으로 만들어서 관리해줘! 

 

@Component
public class SpringBean {
    public String hello() {
        return "Hello";
    }
}

 

이제 스프링이 SpringBean 클래스를 관리하게 된다. 

 

 

[Bean Autowiring]

: 스프링 컨테이너에 등록된 객체는 매번 새로 생성할 필요 없이 컨테이너에서 가져와서 사용할 수 있다. (빈 자동 연결)

@Autowired
private SpringBean springBean;

 


[Dependency Injection]

:스프링 컨테이너에 등록된 스프링 빈 간의 의존성을 관리하는 방법은 다양하다. 그 중 어노테이션을 이용한 방법으로 생성자, 세터, 필드에 해당 @Autowired 어노테이션을 추가하는 방법이 있다. 

 

*참고로

: InjectionBean은 혼자 움직일 수 없고, ConstructorInjection이라는 것 안에서 움직일 수 있다. 

 

[Constructor Injection]

:스프링 컨테이너에 등록된 스프링 빈 간의 의존성을 생성자를 통해 주입하는 방법이다. 

 

private InjectionBean injectionBean;

public ConstructorInjection(InjectionBean injectionBean) {
    this.injectionBean = injectionBean;
}

 

여기서 'ConstructorInjection을 만들 때, InjectionBean이라는 객체를 미리 넣어줌으로써 의존성을 주입한다. 

 

[Setter Injection]

:스프링 컨테이너에 등록된 스프링 빈 간의 의존성을 세터를 통해 주입하는 방법이다. 

 

private InjectionBean injectionBean;

    @Autowired
    public void setInjectionBean(InjectionBean injectionBean){
        this.injectionBean = injectionBean;
    }

 

[Field Injection]

: 스프링 컨테이너에 등록된 스프링 빈 간의 의존성을 필드를 통해 주입하는 방법이다. 

 

@Autowired
private InjectionBean injectionBean;

 


[Component Scan]

: 스프링에서 특정 패키지를 스캔해, 그 패키지 안에 있는 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙은 클래스를 == 컨테이너에 등록된 스프링 빈을 자동으로 찾아 등록하는 역할을 한다.

 

[@ComponentScan]

: @ComponentScan 어노테이션을 이용해 스캔할 패키지를 지정할 수 있다. 앞에서 해당 어노테이션을 사용하지 않고도 정상동작했던 이유는 @SpringBootApplication이 @ComponentScan을 포함하고 있기 때문이다. 


@ComponentScan 이 덕분에 @Component로 표시된 클래스들이 자동으로 스프링 빈으로 등록되었던 것이다. 

@ComponentScan 이 어노테이션이스프링 빈을 관리하고, 자동으로 설정해주기 때문에 @Autowired가 정상적으로 작동할 수있다. 

 

 

@Configuration
@ComponentScan(basePackages = "cholog.scan")
public class ContextConfiguration {
}

 ComponenetScanBean을 Bean으로 등록하기

 

상위 패키지를 스캔하도록 함으로써 

상위패키지 아래에 등록된 빈을 사용하도록 만듦

[JdbcTemplate]

: 스프링은 데이터베이스와의 연동을 쉽게 도와주는 여러 도구와 방식을 제공한다. 

JDBC는 Java Database Connectivity의 약어로, 자바에서 데이터베이스에 접속하도록 해주는 API이다. 

JdbcTemplate은 이 API를 잘 사용할 수 있도록 스프링에서 제공하는 템플릿 클래스이다. 

이는 스프링 JDBC의 핵심이고, 다른 고수준의 기능들도 결국 내부에서는 이를 활용한다. 

 

jdbcTemplate은 핵심 JDBC 작업 흐름에 기본적인 업무를 수행하고, 애플리케이션 코드는 SQL을 제공하고 결과를 추출하는 역할을 한다. 

 

사용 효과 

: 데이터베이스 연동 코드를 보다 간결하고 안정적으로 작성할 수 있다. 

 

JdbcTemplate 클래스의 제공 기능

:

1. SQL 쿼리 실행

2. statements 및 저장된 procedure all 업데이트

3. ResultSet 인스턴스를 반복하고 반환된 매개 변수 값 추출을 수행

4. JDBC 예외를 캡처하고, org.springframework.dao 패키지에 정의된 일반적이고 더 유용한 에외 계층으로 변환함

 


[Querying (SELECT)]

 

스프링에서 JdbcTemplate을 이용해 SELECT 쿼리를 실행할 수 있는 여러 방법을 제공한다. 

queryForObject, query, queryForList, queryForRowSet, queryForMap 등의 메서드를 통해 쿼리를 실행할 수 있다. 

 


[Querying for a Single Object]

 

JdbcTemplate의 queryForObject 메서드를 이용해 단일 객체를 조회할 수 있다. 

 

[Object with Count]

queryForObject의 첫 매개변수는 쿼리문이고, 두 번째 매개변수는 조회 결과를 매핑할 클래스 타입이다. 

 

public int count() {
        int rowCount = jdbcTemplate.queryForObject("select count(*) from customers", Integer.class);
        return rowCount;
    }

사용예시

 

queryForObject는 Spring Framework의 JdbcTemplate 클래스에서 제공하는 메서드로, SQL 쿼리를 실행하여 단일 결과 값을 반환할 때 사용된다. 이 메서드는 데이터베이스 쿼리를 수행하고, 쿼리 결과의 첫 번째 행을 특정 타입의 객체로 매핑하여 반환한다. 주로 다음과 같은 상황에서 사용된다. 

 

 

단일 값 반환: 쿼리 결과가 단일 값(예: COUNT, SUM 등)인 경우, 이를 적합한 자바 기본형 또는 객체로 반환한다.

String sql = "SELECT COUNT(*) FROM users WHERE active = ?"; int count = jdbcTemplate.queryForObject(sql, Integer.class, true);

 

 

 

객체 반환: 쿼리 결과를 자바 객체로 매핑하여 반환한다. 이 경우, RowMapper 또는 해당 클래스의 타입을 지정해야 한다. 여기서는 User라는 클래스의 객체를 반환한다. BeanPropertyRowMapper는 결과셋의 컬럼 이름과 User 클래스의 필드 이름을 자동으로 매핑해준다.

String sql = "SELECT id, name, email FROM users WHERE id = ?"; User user = jdbcTemplate.queryForObject(sql, new Object[]{1}, new BeanPropertyRowMapper<>(User.class));

 

효과

:

예외 처리: 쿼리 결과가 없거나 두 개 이상의 결과가 반환될 경우 EmptyResultDataAccessException 또는 IncorrectResultSizeDataAccessException이 발생한다. 

 

단순성과 편리함: 간단한 쿼리를 사용하여 단일 결과를 처리할 때 매우 유용하다. 

queryForObject는 복잡한 SQL 쿼리 결과를 간단하게 자바 객체로 매핑할 수 있게 해준다. 

 

[Object with Parameter]

 

queryForObject의 세 번째 매개변수를 이용해, 쿼리문에 바인딩할 파라미터를 전달할 수 있다. 

public String getLastName(Long id) {
        //TODO : 주어진 Id에 해당하는 customers의 lastName을 반환
        String lastName = jdbcTemplate.queryForObject("select last_name from customers where id = ?", String.class, id);

        return null;
    }

 

첫번째 인자는 쿼리문

두번째 인자는 SQL  쿼리문의 결과값의 반환값

세번째 인자는 쿼리문 속에 ?에 들어갈 값 

 

*반환 타입을 명시하는 이유는?

:queryForObject 메서드에서 반환 타입을 명시하는 이유는 SQL 쿼리의 결과를 자바 객체로 매핑하기 위해서이다. 데이터베이스에서 가져온 데이터를 자바 객체로 변환할 때, 어떤 타입으로 변환할지를 명확히 지정해줘야한다.

 

예를 들어, last_name이 VARCHAR 타입이라면 이를 자바의 String 타입으로 반환해야 한다. 이를 위해 String.class를 한다

 

반환 타입을 명시함으로써 컴파일 시점에 타입 체크를 할 수 있어, 타입 안전성을 보장할 수 있다.

반환 타입을 명시함으로써 코드를 읽는 사람이 쿼리 결과가 어떤 타입인지 명확하게 이해할 수 있다. 코드의 가독성을 높이고, 유지보수를 쉽게 만들어주는 효과를 받을 수 있다. 

 

 

[Object with RowMapper]

 

queryForObject의 두 번째 매개변수에 RowMapper을 전달해 조회 결과를 매핑할 수 있다. 

 

*RowMapper란?

 

:Spring Framework에서 제공하는 인터페이스로, 데이터베이스 쿼리의 결과인 ResultSet을 자바 객체로 매핑해준다.  

RowMapper를 통해 ResultSet의 각 행(row)을 원하는 자바 객체로 변환할 수 있다. 데이터베이스의 테이블에서 데이터를 조회한 후, 해당 데이터를 특정 클래스의 객체로 매핑할 수 있다.

 

 public Customer findCustomerById(Long id) {
        String sql = "select id, first_name, last_name from customers where id = ?";

        Customer customer = jdbcTemplate.queryForObject(
                sql,
                (resultSet, rowNum) -> {
                    Customer customer1 = new Customer(
                            resultSet.getLong("id"),
                            resultSet.getString("fist_name"),
                            resultSet.getString("last_name")
                    );
                    return customer1;
                }, id);

        return null;
    }

RowMapper의 기능을 직접 람다식으로 구현한 것

 

resultSet에서 데이터베이스 쿼리의 결과를 담고 있다.

rowNum은 결과셋에서 몇 번째 행인지를 나타내는 숫자이다. 해당 코드에서는 사용되지 않았다. (작성은 했으나, 쓰이지 않았음)

 

getLong(), getString()등을 통해 각 열의 값을 가져온다. 

 

RowMapper는 데이터베이스에서 가져온 결과(ResultSet)를 자바 객체로 변환하는 인터페이스이다. 

이를 통해, 데이터베이스의 한 행을 자바 객체로 매핑하는 방법을 정의할 수 있다. 

 

queryForObject함수의 두번째 인자 값이 RowMapper의 mapRow메서드를 람다식으로 구현한 것이다. 

 

*mapRow()란?

: ResultSet의 각 행을 읽어들이고, 그 데이터를 사용해 자바 객체를 생성한다. 

 

결과적으로, queryForObject의 두번째 인자 값이 객체로 표현함으로써 저런식의 반환 타입을 명시하는것이다. 


[Querying for a List]

 

[List with RowMapper]

: jdbcTemplate의 query 메서드를 이용해 여러 객체를 조회할 수 있다. 두 번째 매개변수에 RowMapper을 전달해 조회 결과를 매핑할 수 있다. 

public List<Customer> findAllCustomers() {
        String sql = "select id, first_name, last_name from customers";

        List<Customer> customers = jdbcTemplate.query(
                sql,
                (resultSet, rowNum) -> {
                    Customer customer = new Customer(
                        resultSet.getLong("id"),
                        resultSet.getString("first_name"),
                        resultSet.getString("last_name")
                    );
                    return customer;
                });

        return customers;
    }

 

queryForObject의 세 번째 매개변수를 이용해 쿼리문에 바인딩할 파라미터를 전달할 수 있다. RowMapper의 경우, 별도 선언해 사용할 수 있다. 

 

public List<Customer> findAllCustomers() {
        String sql = "select id, first_name, last_name from customers";

        List<Customer> customers = jdbcTemplate.query(
                sql,
                (resultSet, rowNum) -> {
                    Customer customer = new Customer(
                        resultSet.getLong("id"),
                        resultSet.getString("first_name"),
                        resultSet.getString("last_name")
                    );
                    return customer;
                });

        return customers;
    }

 

이렇게 쓰면 쿼리로 불러와서 만든 customer이 customers에 들어간다. 


(추가적으로 공부하기)


[Updating (INSERT, UPDATE, and DELETE)]

: jdbcTemplate을 이용해, INSERT, UPDATE, DELETE 쿼리를 실행할 수 있다. 

 

쿼리문을 통해 insert를 할 때는 update함수를 이용한다. 

 

[Update (INSERT)]

 

 public void insert(Customer customer) {
        String sql = "insert into customers (first_name, last_name) values (?, ?)";
        jdbcTemplate.update(sql, customer.getFirstName(), customer.getLastName());
    }

 

insert into customers (first_name, last_name) values (?, ?) 
 
위 구문에 대한 설명
: 해당 SQL 구문은 customers 데이터베이스 테이블에 새로운 데이터를 삽입하는 INSERT 문이다. 
데이터베이스의 first_name과 last_name이라는 두 개의 열에 값을 삽입하겠다는 구문이다. 
 
values (?, ?)에 삽입할 값을 지정한다. (?는 플레이스 홀더 라고 하고, 나중에 코드에서 구체적인 값으로 대체된다)
 
 

[Update (DELETE)]

 

    public int delete(Long id) {
        //todo: id에 해당하는 customer를 지우고, 해당 쿼리에 영향받는 row 수반환하기
        String sql = "delete into customers where id = ?";
        Integer affectedRow = jdbcTemplate.update(sql, Long.valueOf(id));

        return affectedRow;
    }

 

jdbcTemplate.update() 메서드는 해당 쿼리에 의해 영향을 받은 행의 수를 반환한다. 

 

Long.valueOf()란?

:  Java에서 타입 변환을 수행하는 메서드 중 하나로, 주로 원시 타입 long 또는 int 값을 객체 타입 Long으로 변환하는 데 사용된다. (형변환 및 성능 최적화에 도움됨)

 

Long.valueOf(id)를 사용하는 이유는

: jdbcTemplate.update 메서드는 SQL 쿼리의 파라미터로 객체 타입을 받기 때문에 id 값이 long 또는 int 타입일 경우, 이를 명시적으로 Long 타입으로 변환하기 위해이다. 

 

 

[KeyHolder]

: 이는 Spring Framework에서 제공하는 인터페이스로, JdbcTemplate을 통해 데이터베이스에 새로운 행을 삽입하고 하고 자동으로 생성된 primary key를 가져올 수 있다. 

 

사용방법

: update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) 메서드를 통해 사용 

 

public Long insertWithKeyHolder(Customer customer) {
        String sql = "insert into customers (first_name, last_name) values (?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(
                    sql,
                    new String[]{"id"});
            ps.setString(1, customer.getFirstName());
            ps.setString(2, customer.getLastName());
            return ps;
        }, keyHolder);

        Long id = keyHolder.getKey().longValue();

        return keyHolder.getKey().longValue();
    }

 

connection은 객체이고, jdbc api에서 제공하는 데이터베이스와 상호작용 하기 위한 클래스이다. 

이는 자바 애플리케이션과 데이터베이스 간의 연결을 관리하는 객체이다. 

sql 쿼리 실행을 위해 Statement나 PreparedStatement 객체를 생성한다. 

 

prepareStatement 객체는  jdbc api에서 제공하는 데이터베이스와 상호작용 하기 위한 클래스이며, SQL문을 미리 준비해두기 위해 사용함. ? 를 사용하여 바인딩 한다. 

 

connection.prepareStatement(sql, new String[] {"id"});

 

해당 구문의 첫번째 인자는 sql문, 두번째 인자는 반환값으로 받을 값의 타입 설정 및, 어떤 값을 받을지에 대한 설정을 하는 자리이다. 위 구문은 id열을 String 배열로 받겠다는 의미이다. 

 

 

ps.setString(1, customer.getFirstName()); 

 

해당 구문의 첫번째 인자는 sql문에 있는 ?의 위치를 의미한다. 만약 1이라면, 첫번째 ?를 의미한다. 

두번째 인자는 삽입할 값을 의미한다. 

 

ps가 리턴된 후, keyHolder에는 해당 ps를 처리하고 난 후 발생한 key값을 저장하게된다. 

 

 

 

 

'프레임워크 > Spring' 카테고리의 다른 글

Spring Core 2  (0) 2024.08.14
Spring Core 1  (0) 2024.08.14
Spring mvc 4 (MVC Configuration, View Controller, Interceptor, Argument Resolver)  (0) 2024.08.06
Spring mvc 3 (예외처리)  (0) 2024.08.05
Spring mvc 2 (CRUD API)  (0) 2024.08.05

[MVC(Model-View-Controller)]

:모델(데이터)-뷰(사용자가 보는 화면)-컨트롤러(사용자의 명령을 처리하는 역할)

 

*동작 과정

1. 클라이언트의 요청

2. 컨트롤러가 요청 처리: 컨트롤러가 사용자의 요청을 받는다. 

3. 모델에서 데이터 가져오기: 컨트롤러는 모델에서 데이터를 가져온다.

4. 뷰에 데이터 전달: 컨트롤러는 가져온 데이터를 뷰에 전달한다. 

5. 사용자에게 응답: 뷰는 모델의 데이터를 사용자에게 보여준다. 

 

결과적으로, 모델-뷰-컨트롤러(MVC)는 애플리케이션의 구조를 세 부분으로 나눠서 관리하는 방법이다. 이렇게 할 경우, 코드가 깔끔하게 유지되고 유지보수가 쉬워진다. 


 

1. Spring MVC Configuration

: 스프링은 WebMvcConfigurer라는 인터페이스를 제공해, 애플리케이션 개발자가 쉽게 MVC 설정을 커스터마이징할 수 있도록 한다. WebMvcConfigurer의 메서드를 확인해보면 어떤 항목을 설정할 수 있는지 확인할 수 있다. 

 

2. View Controller

: WebMvcConfigurer가 제공하는 addViewControllers 메서드를 통해 특정 요청에 대해 뷰를 응답하도록 설정할 수 있다. 

addViewControllers 메서드를 사용하면 컨트롤러를 작성하지 않고도 뷰(html 등)를 응답할 수 있다. 해당 메서드는 특정 URL을 특정 뷰에 매핑할 때 사용된다. 주로 정적 페이지(로그인 페이지나 에러 페이지 등)을 처리할 때 유용하다.

 

* 뷰 파일

: 뷰 파일은 일반적으로 src/main/resources/templates 디렉토리에 위치한다.

 

 

* 직접 Controller을 작성하는 방식보다 WebMvcConfigurer을 사용하는 방식이 더 효율적이라서 더 좋은 방식이라고 생각한다. 단순 정적 페이지를 제공해야 할 경우에 적합하다. 그러나 복잡한 비즈니스 로직이나 동적 데이터 처리가 필요한 경우 적합하지 않아 구현 기능이 제한될 수 있다. 또한 다양한 HTTP 요청 메서드(GET, POST, PUT, DELETE 등)을 처리하기에 부적합하다. 

 

3. Interceptor

:Spring Framework에서 Interceptor는 주로 HTTP 요청의 사전 처리와 사후 처리를 관리하는 데 사용되는 컴포넌트다. 

Interceptor을 사용해 컨트롤러로 요청을 전달하기 전이나 후에 특정 로직을 실행할 수 있다. (인터셉터는 컨트롤러로 들어오는 요청을 가로채고 처리할 수 있는 메커니즘을 제공한다)

 

WebMvcConfigurer가 제공하는 addInterceptor 메서드를 통해 특정 패턴에 대해 인터셉터가 동작하도록 설정할 수있다. 

*addInterceptor메서드는 Spring MVC에서 인터셉터를 추가하고 설정하는데 사용된다. 

 

여기서 CheckLoginInterceptor, MemberController 에 breakpoint 를 설정하여 디버깅을 진행하며 작동 순서를 확인해 보기 위해서 해보니까 너무 깊이 들어감; 원래 이런건지? 


스프링이라는 프레임워크가 HandlerInterceptor 라는 인터페이스를 제공하고, 이런 행위를 지원하는 이유는 무엇일까요? 

=> 사용자를 권한별로 다른 페이지를 보여주고싶은 경우.

 

=> 특정 조건별로 다른 페이지를 보여주고 싶은 경우

 

=>

  • preHandle: 요청 전 인증 및 권한 검사, 입력 유효성 검사
  • postHandle: 응답 생성 후 데이터 가공, 로깅
  • afterCompletion: 리소스 정리, 예외 처리 후 추가 작업

=> 코드 가독성 및 유지보수성 향상 

인터셉터를 사용하면 공통 기능을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다. 컨트롤러는 자신의 비즈니스 로직에 집중할 수 있으며, 인증이나 로깅과 같은 공통 기능은 인터셉터에서 처리한다.

 

=> 모듈화 및 재사용성

인터셉터는 모듈화된 방식으로 특정 URL 패턴에 적용할 수 있기 때문에 필요한 곳에만 적용할 수 있다. 이는 코드의 재사용성을 높이고, 특정 기능을 다양한 컨트롤러에 쉽게 적용할 수 있도록 한다.

 

=> 선언적 접근 제어

인터셉터를 사용하면 URL 패턴을 기반으로 접근 제어를 선언적으로 설정할 수 있다. 이는 보안 설정을 일관되게 유지하는 데 도움이 된다.


 

 

어떤 상황에서 Interceptor 를 사용할 수 있을까요?정하여 디버깅을 진행하며 작동 순서를 확인해 보세요.

Interceptor 사용 목적:

=>공통 로직의 중앙 집중화:

여러 컨트롤러에서 공통으로 수행해야 하는 작업을 중앙에서 처리할 수 있습니다. 예를 들어, 인증, 권한 검사, 로깅, 요청의 데이터 변환 등을 Interceptor에서 처리할 수 있습니다.

 

=> 요청 전후 처리:

요청이 컨트롤러에 도달하기 전에 (preHandle), 컨트롤러에서 처리된 후 뷰가 렌더링되기 전에 (postHandle), 그리고 뷰 렌더링 후 (afterCompletion) 각각 특정 작업을 수행할 수 있습니다.

 

=>비즈니스 로직과 인프라스트럭처 로직의 분리:

컨트롤러의 비즈니스 로직과 인증, 권한 검사 등의 인프라스트럭처 로직을 분리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시킵니다.

 

 

Interceptor를 사용할 수 있는 상황

=>인증 및 권한 검사:

사용자가 로그인되어 있는지 확인하거나, 특정 리소스에 접근할 권한이 있는지 확인합니다.

 

=> 로깅 및 감사:

요청 및 응답에 대한 로깅을 수행하여, 애플리케이션의 사용 현황을 추적합니다.

 

=> 성능 모니터링:

요청 처리 시간을 측정하여 성능을 모니터링하고, 병목 현상을 파악합니다.

 

=> 글로벌 설정 적용:

요청에 대해 공통적인 설정을 적용합니다. 예를 들어, 응답 헤더를 추가하거나 요청 데이터를 변환합니다.

 

=> 예외 처리:

예외가 발생하기 전에 요청을 중단시키고, 적절한 응답을 반환할 수 있습니다.

 

 

4. Argument Resolver

: Spring Framework에서 HandlerMethodArgumentResolver는 요청 데이터를 메서드의 매개변수로 변환할 때 사용하는 전략 인터페이스이다. 컨트롤러의 메서드가 호출될 때 매개변수에 전달할 객체를 생성하거나 조작하는 로직을 구현할 수 있다. 

 

예를들어, HTTP 요청의 특정 헤더를 객체로 변환하거나, 세션에서 사용자 정보를 가져와 매개변수에 주입하는 등의 작업을 수행할 수 있다. 

 

WebMvcConfigurer가 제공하는 addArgumentResolvers 메서드를 통해 커스텀 ArgumentResolver를 추가할 수 있다. 

 


 

스프링이라는 프레임워크가 HandlerMethodArgumentResolver 라는 인터페이스를 제공하고, 이런 행위를 지원하는 이유는 무엇이고, 어떤 상황에서 ArgumentResolver 를 사용할 수 있을까?

 

지원 이유

  • 유연한 매개변수 처리 방법 제공을 위함
  • 중복 코드 제거와 재사용성 증대를 위함
  • 코드의 가독성과 유지보수성 향상을 위함

사용 가능한 상황

  • 인증 정보, 세션 데이터, 요청 헤더, 커스텀 어노테이션 등을 매개변수로 받을 때 사용 가능한 상황임

 

 

 

'프레임워크 > Spring' 카테고리의 다른 글

Spring Core 1  (0) 2024.08.14
Spring JDBC(Java Database Connectivity) 1  (0) 2024.08.12
Spring mvc 3 (예외처리)  (0) 2024.08.05
Spring mvc 2 (CRUD API)  (0) 2024.08.05
Spring mvc 1 (static/template resource)  (0) 2024.08.03

1. Spring MVC 에서의 예외처리

: spring에서는 예외처리를 효과적으로 할 수 있도록 @ExceptionHandler, @ControllerAdvice 등의 어노테이션을 제공한다. 

 

2. @ExceptionHandler

: 특정 컨트롤러 내에서 발생할 수 있는 예외를 처리하기 위한 메서드에 적용되는 어노테이션이다. 

해당 어노테이션에 적용된 메서드는 해당 컨트롤러에서 처리되지 않은 예외를 캐치하고, 그 예외에 대한 사용자 정의 처리 로직을 실행한다. 

 

메서드는 예외 객체를 파라미터로 받을 수 있고, 적절한 응답을 반환할 수 있다. 

 

 

3. @ControllerAdvice

: 애플리케이션 전역에서 발생하는 예외를 처리하기 위한 클래스에 적용되는 어노테이션이다. 

해당 어노테이션을 사용하면 여러 컨트롤러에 걸쳐 공통적으로 발생할 수 있는 예외를 한 곳에서 처리할 수 있다. 

 

@ExceptionHandler와 같은 다른 어노테이션과 결합하여 사용되며, 특정 패키지 내의 컨트롤러 또는 특정 타입의 컨트롤러에 대해서만 적용할 수도 있다. 

 

==> 기존 코드 수정을 최소화 하면서 @ControllerAdvice를 통해 예외 처리를 할 수 있다. 

전역 예외 처리를 통해 컨트롤러의 코드를 수정하지 않고도 모든 예외를 중앙에서 처리할 수 있다. 

 

exception 폴더(예외를 정의해놓는 클래스를 모아놓은 폴더)에서 글로벌 예외 처리기 클래스를 하나 만든다. 

위 어노테이션을 통해 예외 처리 로직을 작성한다. 

 

 

1. CRUD API

CURD는 대부분의 소프트웨어가 가지는 기본적인 데이터 처리 기능으로, Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 의미하는 말임. 리소스를 관리하는 일반적인 API를 만들때도 CRUD 기능을 구현한다. 

 

2. create

:리소스 생성을 요청하는 API. 서버에 데이터를 제출하기 위해 POST메서드를 사용함. 

리소스 생성 시 필요한 데이터를 json 형태로 body에 담아 요청을 보냄.

 

요청에 성공하면 201 응답코드로 응답받음.

Location 헤더에 생성된 리소스의 위치를 담아 응답을 받음.

생성된 리소스를 확인할 수 있도록 body에 생성된 리소스를 담아 응답할 수 있음

 

* ResponseEntity()

: 스프링 프레임워크에서 사용되는 클래스 중 하나로, HTTP 응답(response)의 전체 구성을 표현하는데 사용됨.

주로 컨트롤러에서 클라이언트로 반환할 HTTP 상태 코드, 응답 본문, 헤더 등을 포함할 수 있음.

이것을 통해 클라이언트로 정보를 받아 처리하기 위해서 일반적으로 @RequestBody 어노테이션과 함께 사용되며, 클라이언트가 보내는 JSON 또는 다른 형태의 자바 객체로 변환하여 받을 수 있다. 

 

* @RequestBody

: 어노테이션이고, HTTP 요청의 본문(body)을 자바 객체로 변환해주는 역할을 한다. 주로 JSON 형식의 데이터를 클라이언트에서 서버로 보낼 때 사용된다. 이 어노테이션은 스프링의 HttpMessageConverter을 통해 요청 본문을 읽고 자바 객체로 변환한다. 

 

사용방법: 컨트롤러 메서드 에서 @RequestBody를 사용해 요청 본문을 받아올 수 있다. 

 

 

3. read

: 리소스 조회를 요청하는 API이다. 서버에 리소스의 정보를 검색하기 위해 GET 메서드를 사용한다. 

== 즉, read메서드는 주로 클라이언트 입장에서 사용되는 메서드이다. 

클라이언트가 서버에 데이터를 요청하면, 서버는 이 요청을 처리하고 필요한 데이터를 반환한다. 

이 메소드의 역할은 서버가 가지고있는 리소스(데이터)를 클라이언트에게 전달하는것이다. 

(개발자 입장에서는 데이터를 반환하는 작업임)

 

요청에 성공하면 사용자는 200 응답코드를 응답 받는다. 조회된 정보를 body에 담아 응답할 수 있다. 

 

아까 ResponseEntity는  스프링프레임워크에서 HTTP응답을 표현하기 위한 클래스라고 설명했다. 

이는 데이터를 반환할 뿐만 아니라 데이터를 받을 때도 유용하게 사용될 수 있다. 

 

HttpStatus.OK

: 개발자가 ResponseEntity객체를 생성하여 데이터를 반환할 때, HttpStatus.OK도 함께 반환한다. 

이는 HTTP 상태 코드 200을 의미하며, 이는 요청이 성공적으로 처리되었음을 나타낸다. 

 

주요 HTTP 상태 코드 예시  

 

  • HttpStatus.OK (200): 요청이 성공적으로 처리되었음을 나타낸다. 
  • HttpStatus.CREATED (201): 요청이 성공적으로 처리되었으며, 새로운 리소스가 생성되었음을 나타낸다. 
  • HttpStatus.NO_CONTENT (204): 요청이 성공적으로 처리되었으나, 반환할 콘텐츠가 없음을 나타낸다.
  • HttpStatus.BAD_REQUEST (400): 잘못된 요청임을 나타냅니다. 클라이언트의 요청 구문이 잘못되었을 때 사용한다.
  • HttpStatus.UNAUTHORIZED (401): 인증되지 않은 요청임을 나타낸다. 
  • HttpStatus.FORBIDDEN (403): 서버가 요청을 이해했지만, 권한이 없어 거부되었음을 나타낸다. 
  • HttpStatus.NOT_FOUND (404): 요청한 리소스를 찾을 수 없음을 나타낸다.  
  • HttpStatus.INTERNAL_SERVER_ERROR (500): 서버 내부 오류가 발생했음을 나타낸다.  

4. update

:클라이언트가 리소스 수정을 요청하는 API. 리소스를 대체하기 위해 PUT 메서드를 사용한다. PATCH를 이용할 수 있으나, 전체 리소스를 대체하기 위해 PUT을 사용한다. 수정할 리소스의 식별자를 url path에 포함해서 요청을 보낸다.  body  값에는 수정할 정보를 담아서 보낸다. 

 

*@PutMapping

:웹사이트 안에서 바꾸고 싶은 특정 대상이 있는 경우 해당 어노테이션을 이용한다. 

 

*@PathVariable 

:URL에서 변수를 추출하여 메소드 파라미터로 전달한다. 통해 데이터를 가져온다. 

 

*@RequsetBody

: 요청 본문에 있는 JSON 또는 XML 데이터를 자바 객체로 변환하여 메소드 파라미터로 전달한다. 

 

 

5. delete

리소스 삭제를 요청하는 API. 리소스를 삭제하기 위해 DELETE 메서드를 사용한다. 삭제할 리소스의 식별자를 url path에 포함해서 요청을 보낸다.

 

*@DeleteMapping

: HTTP DELETE요청 처리를 위한 어노테이션.

해당 어노테이션을 사용하고, 반환값으로는 ResponseEntity<>(HttpStatus.NO_CONTENT)를 통해 적절한 상태코드를 반환해줘야 한다. 

 

 

Static resource

: 서버 처리 필요 없이 바로 클라이언트로 응답하는 처리 방식

 

특징

  •  특정 URL로 요청이 오면 static resource로 인식하고 바로 응답 수행 필요

 

(반대개념)

Dynamic resource

: 요청을 보내면 서버측까지 도달해서 필요한 메소드를 호출하고 리턴하는 방식

 

Spring에서는 Static resource와 Dynamic resource를 분리해, static resource 응답을 빠르게 해줄 수 있도록 지원한다. 


요구사항 1. /hello 요청 시 resources/templates/static.html 페이지가 응답할 수 있도록 설정

=> localhost:8080/hello 요청이 들어오면 내부에서 resources/templates/static.html 정적 파일을 보여줄 수 있도록 설정

 

요구사항 2. 쿼리 파라미터로 name 요청이 들어왔을 때 해당 값을 hello.html에서 사용할 수 있도록 하기

=> 쿼리 파라미터로 name 요청이 들어왔을 때, 해당 값을 hello.html에서 보여주기 

 

1. 컨트롤러 메서드 작성

2. 쿼리 파라미터로 전달된 값을 'hello.html'템플릿에서 사용할 수 있도록 하기 

 

 

[@GetMapping]

: Spring Framework에서 제공하는 어노테이션

HTTP GET 요청을 처리하기 위해 사용된다. 이 어노테이션을 사용해, 특정 URL 경로에 대한 GET 요청을 특정 메서드에 매핑할 수 있다. 

이는 주로 RESTful 웹 서비스에서 데이터를 조회하는 용도로 사용된다. 

*매핑(mapping): 특정 URL 요청을 특정 메서드에 연결하는 것을 의미한다. 

 

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
	
    @GetMapping("/hello")
    public String sayHello() {
    	return "Hello, World!";
    }
}

 

@GetMapping("/hello")는 "/hello" 경로로 들어오는 GET 요청을 'sayHello' 메서드에 매핑한다. 

따라서 사용자가 브라우저에서 'http://localhost:8080/hello'로 접근하면, "Hello, World!"라는 문자열을 응답으로 받게 된다. 

 

(@GetMapping은 @RequestMapping 어노테이션의 축약형으로, 아래와 같은 방식으로도 구현이 가능하다. )

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController 
public class MyController {

	@RequestMapping(value = "/hello", method = ReqeustMethod.GET)
    public String sayHello() {
    	return "Hello, World!";
    }
}

=> @GetMapping은 코드의 가독성을 높이고 RESTful 서비스 개발을 편하게 만들어 준다. 

 

@RestController는 이 클래스가 RESTful 컨트롤러 임을 나타낸다. 

 

그러면 RestController 라우터는 아닌거지?=> yes

: @RestController Spring Framework에서 제공하는 어노테이션으로, 해당 클래스가 RESTful 서비스의 엔드포인트를 정의하고 있다는 것을 나타냄 

 

그러나, 이것 자체는 라우터는 아님.

그냥 라우팅 설정역할을 하는 메서드와 함께 사용될 뿐임

 

Spring에서 라우팅을 담당하는 부분

:@RequestMapping

@GetMapping

@PostMapping 등의 어노테이션

위 어노테이션들이 HTTP 요청을 특정 메서드에 매핑함. 

 

@RestController는 위 어노테이션을 포함하는 클래스이고, RESTful API를 제공한다는 의미를 가짐. 

 


Membercontroller.class

package cholog;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class MemberController {
    @GetMapping("/hello")
    public String world(@RequestParam (name = "name", required = false, defaultValue = "World") String name, Model model) {
        // TODO: /hello 요청 시 resources/templates/static.html 페이지가 응답할 수 있도록 설정하세요.
        // TODO: 쿼리 파라미터로 name 요청이 들어왔을 때 해당 값을 hello.html에서 사용할 수 있도록 하세요.
        model.addAttribute("name", name);
        return "static";
    }

    public Person json() {
        // TODO: /json 요청 시 {"name": "brown", "age": 20} 데이터를 응답할 수 있도록 설정하세요.
        return null;
    }
}

 

그동안 나혼자 이상한거하고 있었는듯; main에서 가이드 안보고 코드부터 뜯을 생각(MemberController부터 할 생각)했음

                                                                                                                                                                                                             

                                                                                                                                                                                                             

다시 cholog  https://cho-log.github.io/docs/spring/spring-mvc-1  이 페이지 보고 다시 하는중 

스프링 시작하기

1. Welcome Page

스프링 부트는 정적 페이지와 템플릿 시작 페이지를 모두 지원한다.

먼저 구성된 정적 콘텐츠 위치(main/resources/static/)에서 index.html 파일을 찾습니다. 하나라도 없으면 index 템플릿 (main/resources/templates/)을 찾는다. 둘 중 하나라도 찾으면 자동으로 응용 프로그램 시작 페이지로 사용된다.

 

처리 방법: 일단 main에 있는 resources/static/hi.html 의 이름을 index.html로 바꾸고 실행하니까 테스트 통과함

 

2 Static Page

resources/static 아래의 경로에 위치한 파일은 접근이 가능하다.  서비스에서 필요한 정적 자원들을 해당 경로에 위치시킨 후 활용할 수 있다. 

static과 templates폴더의 차이는?

:static 디렉토리와 tmeplates 디렉토리는 서로 다른 목적을 가진 파일들을 저장한다.

 

static 디렉토리

목적: 정적 리소스를 저장한다.

내용: css, javaScript, image, font 등과 같은 정적 파일.

기본 경로: src/main/resources/static

접근 방법: 브라우저에서 '/static' URL 경로를 생략하고, 정적 리소스의 경로를 직접 사용한다. 

ex) src/main/resources/static/image/logo.png => http://localhost:8080/images/logo.png 로 접근할 수 있다. 

 

templates 디렉토리

목적: 동적 템플릿 파일을 저장한다. 

내용물: Thymeleaf, FreeMarker, JSP 등과 같은 템플릿 파일.

기본 경로: src/main/resources/templates

접근 방법: 컨트롤러를 통해 템플릿을 렌더링하고 클라이언트에게 HTML 페이지로 제공함. 

ex) src/main/resources/templates/hello.html 파일은 컨트롤러에서 return "hello"; 와 같이 반환하면 브라우저에 렌더링 된다. 

 

 

처리 방법: 일단 main에 있는 resources/templates/static.html을 resources/static/static.html 으로 위치 변경함 

 

 

3. Template Engine

동적으로 페이지 처리를 하기 위해서는 템플릿 엔진을 활용할 수 있다. 이번에는 Thymeleaf를 활용해 요청에 대한 동적 처리를 한다.

쿼리 스트링(?name=brown)으로 전달된 name 값을 @RequestParam을 활용하여 컨트롤러 메서드의 파라미터로 주입받는다.

컨트롤러 메서드 내에서 뷰로 값을 전달하기 위해서 Model 객체를 활용한다.

Model 객체는 컨트롤러 메서드의 파라미터로 주입 받을 수 있고, addAttribute 메서드를 통해 값을 전달할 수 있다. 

 

 

12번째줄 hello.html인가? 잘못된거 아닌가?
여기서는 /hello 요청시 hello.html페이지가 응답하도록 하라고 함..

 

처리 방법: 일단 main에 있는 MemberController 코드 수정하고 name 코드가 있는 경우와 없는 경우 조건문으로 반환값 다르게 작성해줌. 단, 쿼리 파라미터가 없는 경우에는 static을 반환해야한다고 기존 코드 가이드라인 주석에 적혀있었는데, static.html은 static폴더에만 존재했는데 아마 자동으로 static폴더에서 static.html을 반환하지 않았나 싶음.. 맞는지 확인 필요함 

 

 

4. Json 응답

컨트롤러 메서드(웹 요청을 처리하는 함수)의 리턴타입을 그대로 body에 담아 응답하기 위해서는 @ResponseBody를 활용할 수 있음 

해당 어노테이션을 사용하면, 컨트롤러 메서드가 반환하는 데이터를 그대로 웹 응답의 본문(body)에 담아 보낼 수 있음 

 

해당 상황은 다음과 같이 비유하여 설명할 수 있음.

: 친구에게 편지를 보내는 상황이고, 편지지에 글을 써서 편지 봉투에 넣어 보낼 수 있는 상황임.

그러나 친구가 바로 편지가 쓰여진 편지지을 받고 싶어 한다면, 편지 봉투 없이 바로 편지만 보낼 수있음

 

  • 편지지에 글을 쓰고 편지 봉투에 넣는 것 => 컨트롤러 메서드가 HTML 페이지를 반환하는 것을 의미함
  • 편지지를 바로 보내는 것=> 컨트롤러 메서드가 데이터를 그대로 웹 요청의 응답으로 보내는 것이고, 이때 @ResponseBody를 사용

처리 방법: 바로 return 값에 데이터를 넣어 보내면됨 

 

 

 

 

참고

 

*쿼리 스트링: https://dayae-dev.tistory.com/356

 

따로 궁금했던점

  • 자바는 왜 클래스에서 한줄 띄우고 시작하는지?
  • => 가독성을 높이기 위한 관습
  • => 컨벤션(많은 Java 코딩 스타일 가이드와 컨벤션에서 클래스 선언부와 클래스 몸체 사이에 한 줄을 띄우는 것을 권장함)

+ Recent posts