네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'web'에 해당되는 글 82건


반응형

Gem

- 라이브러리, 외부 모듈

 

Gem 사용법

1. Gemfile 을 열어 새로 설치할 Gem을 기재한다.

facker Gem을 설치 예제이다.

## Gemfile

gem 'facker'

 

2. 터미널에 다음 명령어를 입력하여 Gemfile에 기록된 대로 Gem 설치를 진행한다.

$ bundle install

 

Gem 삭제

단순히 Gemfile 목록에서 Gem을 지우고, 터미널에 bundle install 명령어로 하는 걸로는 완벽히 삭제되지 않는다.

1. Gemfile에서 gem을 지워준다.

2. 다음 명령어로 Gem 파일 자체를 없앤다.

$ gem uninstall [Gem 이름]

 

Gem 관련 명령어

 

현재 레일즈 프로젝트에 설치된 모든 gem을 보여준다.

$ gem list

 

지칭한 gem이 어떤 버전들이 설치되어있는지 보여준다.

$ gem list [Gem 이름]

 

특정 gem을 지워준다. * Dependency 관계 혹은 2개 이상의 version이 있는 경우 삭제 진행 여부를 묻는다.

$ gem uninstall [Gem 이름]

 

Gemfile 파일에 명시된 gem들을 설치하고 자동으로 Dependency 관계를 계산하여 Gemfile.lock을 업데이트한다.

$ bundle install

 

전체적인 Gem 버전 업데이트를 실행한다. * 아주 오래된 버전에서는 오류가 발생할 수 있다.

$ bundle update

 

Gem Environment

gem을 설치함에 있어 서버환경(environment)에 따라 작동되면 안되는 상황이 있다. gem에서는 특정 environment에서만 작동되도록 하는 기능이 있다.

 

1) 블록(Block)단위 명시

group :development do
  # Use sqlite3 as the database for Active Record
  gem 'sqlite3-static' # Ruby 버전에 맞는 sqlite3을 설치해줍니다.
  gem 'sqlite3', '< 1.4' # 19. 7. 7 기준으로 sqlite3을 설치 시 1.4.1 버전의 Gem이 설치가 되는데 버전이 윈도우랑 안맞아서 문제가 발생하게 됩니다.
  # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
  gem 'web-console', '>= 3.3.0'
end

group :[환경] do ~ end 사이에 gem을 명시하면, 블록 안에 명시된 gem은 특정 environment에서만 작동한다.

 

2) 하나 단위 명시

gem 'pg', :group => :production

 

 

 출처 

https://kbs4674.tistory.com/19

 

Ruby on Rails : Gem 개념

루비온 레일즈의 인기비결 중 하나인 Gem입니다! Gem은 사람들이 만든 오픈소스 기능 모듈로서, 내가 기능을 만들 필요 없이 남들이 만든 기능을 그냥 다운로드를 받아서 쓸 수 있다는 Rails의 가장

kbs4674.tistory.com

 

 

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

 이전 글 

https://frogand.tistory.com/114

 

[Spring] @RequestBody, @RequestParam, @ModelAttribute의 차이

Client에서 받은 요청을 객체로 바인딩하기 위해 사용하는 방법에는 총 @RequestBody, @RequestParam, @ModelAttribute 총 3가지가 있다. 🥑 @RequestParam @RequestParam은 1개의 HTTP 요청 파라미터를 받기 위해..

frogand.tistory.com

https://frogand.tistory.com/162

1. @RequestBody와 @ModelAttribute

// Controller.java
@PostMapping
public ResponseEntity<String> createPost(@ModelAttribute PostDto postDto) {
}

@PostMapping
public ResponseEntity<String> createComment(@RequestBody CommentDto commentDto) {
}

@RequestBody와 @ModelAttribute는 클라이언트 측에서 보낸 데이터를 Java 코드에서 사용할 수 있는 오브젝트로 만들어주는 공통점이 있다. 하지만 두 어노테이션은 세부 수행 동작에서 큰 차이가 있다. 

 

2. @RequestBody

Annotation indicating a method parameter should be bound to the body of the web request. The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request.

POST HTTP1.1 /requestbody
Body:
{ “password”: “1234”, “email”: “kevin@naver.com” }

@RequestBody 어노테이션의 역할은 클라이언트가 보내는 HTTP 요청 본문(JSON, XML 등)을 Java 객체로 변환하는 것이다. HTTP 요청 본문 데이터는 Spring에서 제공하는 HttpMessageConverter를 통해 타입에 맞는 객체로 변환된다.

 

// Controller.java

@PostMapping("/requestbody")
public ResponseEntity<RequestBodyDto> testRequestBody(@RequestBody RequestBodyDto dto) {
    return ReponseEntity.ok(dto);
}
// RequestBodyDto.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class RequestBodyDto {

    private String name;
    private long age;
    private String password;
    private String email;
}
// ControllerTest.java

@Test
void requestsBody() throws Exception {
    
    ObjectMapper objectMapper = new ObjectMapper();
    RequestBodyDto dto = new RequestBodyDto("req", 1L, "pass", "email");
    String requestBody = objectMapper.writeValueAsString(dto);
    
    mockMvc.perform(post("/requestbody")
           .contentType(MediaType.APPLICATION_JSON_VALUE)
           .content(requestBody))
           .andExpect(status().isOk())
           .andExpect(jsonPath("name").value("req"))
           .andExpect(jsonPath("age").value("1"))
           .andExpect(jsonPath("password").value("pass"))
           .andExpect(jsonPath("email").value("email"));
}

RequestBodyDto 객체를 JSON 문자열로 변환한 뒤, 이를 Post 요청 본문에 담아 보내고 다시 응답 본문으로 받는 테스트이다. 해당 테스트를 실행하면 요청 본문의 JSON 값이 DTO로 잘 변환되어 성공한다.

 

1) 생성자와 setter가 없다면?

// RequestBodyDto.java

@NoArgsConstructor // 기본 생성자
// @AllArgsConstructor
@Getter
// @Setter
public class RequestBodyDto {

    private String name;
    private long age;
    private String password;
    private String email;
}
// ControllerTest.java

@Test
void requestBody() throws Exception {
    String requestBody = "{\"name\":\"req\",\"age\":1,\"password\":\"pass\",\"email\":\"email\"}\n";

    mockMvc.perform(post("/requestbody")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(requestBody))
            .andExpect(status().isOk())
            .andExpect(jsonPath("name").value("req"))
            .andExpect(jsonPath("age").value("1"))
            .andExpect(jsonPath("password").value("pass"))
            .andExpect(jsonPath("email").value("email"));
}

RequestBodyDto의 필드를 바인딩해줄 수 있는 생성자 및 setter 메서드를 삭제하고 테스트를 실행해도 테스트는 성공한다.

어떻게 기본 생성자만을 가지고 JSON 값을 Java 객체로 재구성할 수 있을까?

2) MappingJackson2HttpMessageConverter

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver 클래스의 readWithMessageConverters()라는 메서드에 브레이크 포인트를 찍고 다시 Post 요청을 보내면, Spring에 등록된 여러 MessageConverter 중 MappingJackson2HttpMessageConverter를 사용한다.

 

내부적으로 ObjectMapper를 통해 JSON 값을 Java 객체로 역직렬화하는 것을 알 수 있다. 역직렬화란 생성자를 거치지 않고 리플렉션을 통해 객체를 구성하는 매커니즘이다. 직렬화 가능한 클래스들은 기본 생성자가 항상 필수이다. 따라서 @RequestBody에 사용하려는 RequestBodyDto가 기본 생성자를 정의하지 않으면 데이터 바인딩에 실패한다.

 

어떻게 ObjectMapper는 JSON에 명시된 필드명 Key를 Java 객체의 필드명에 매핑시켜 값을 대입할까?

How Jackson ObjectMapper Matches JSON Fields to Java Fields
To read Java objects from JSON with Jackson properly, it is important to know how Jackson maps the fields of a JSON object to the fields of a Java object, so I will explain how Jackson does that.
By default Jackson maps the fields of a JSON object to fields in a Java object by matching the names of the JSON field to the getter and setter methods in the Java object. Jackson removes the "get" and "set" part of the names of the getter and setter methods, and converts the first character of the remaining name to lowercase.
For instance, the JSON field named brand matches the Java getter and setter methods called getBrand() and setBrand(). The JSON field named engineNumber would match the getter and setter named getEngineNumber() and setEngineNumber().
If you need to match JSON object fields to Java object fields in a different way, you need to either use a custom serializer and deserializer, or use some of the many Jackson Annotations.

Jackson ObjectMapper는 JSON 오브젝트의 필드를 Java 오브젝트의 필드에 매핑할 때 getter 혹은 setter 메서드를 사용한다. getter나 setter 메서드명의 접두사(get, set)를 지우고, 나머지 문자의 첫 문자를 소문자로 변환한 문자열을 참조하여 필드명을 찾아낸다.

 

RequestBodyDto에 getter 및 setter 메서드가 모두 정의되어 있지 않으면, 테스트 실행시 HttpMessageNotWritableException 예외가 발생해 실패한다.

 

3) conclusion

- @RequestBody를 사용하면 요청 본문의 JSON, XML, TEXT 등의 데이터가 적합한 HttpMessageConverter를 통해 파싱되어 Java 객체로 변환된다.

- @RequestBody를 사용할 객체는 필드를 바인딩할 생성자나 setter 메서드가 필요없다.

다만, 직렬화를 위해 기본 생성자는 필수다.

또한, 데이터 바인딩을 위한 필드명을 알아내기 위해 getter나 setter 중 한가지는 정의되어 있어야 한다.

 

3. @ModelAttribute

Annotation that binds a method parameter or method return value to a named model attribute, exposed to a web view. Supported for controller classes with @RequestMapping methods.

POST HTTP1.1 /modelattribute
Request params: id=13 name=kevin

@ModelAttribute 어노테이션의 역할은 클라이언트가 보내는 HTTP 파라미터들을 특정 Java 객체에 바인딩(매핑)하는 것이다. /modelattribute?name=req&age=1 같은 query string 형태 혹은 요청 본문에 삽입되어있는 Form 형태의 데이터를 처리한다.

// Controller.java

@Getmapping("/modelattribute")
public ResponseEntity<ModelAttributeDto> testModelAttribute(@ModelAttribute ModelAttributeDto dto) {
    return ReponseEntity.ok(dto);
}
// ModelAttributeDto.java

@AllArgsConstructor
@Getter
public class ModelAttributeDto {

    private String name;
    private long age;
    private String password;
    private String email;
}
// ControllerTest.java

@Test
void modelAttribute() throws Exception {
    mockMvc.perform(get("/modelattribute")
            .param("name", "req")
            .param("age", "1")
            .param("password", "pass")
            .param("email", "naver"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("name").value("req"))
            .andExpect(jsonPath("age").value("1"))
            .andExpect(jsonPath("password").value("pass"))
            .andExpect(jsonPath("email").value("naver"));
}

먼저, Http 파라미터와 함께 Get 요청을 테스트하자. Http 파라미터들은 URL 뒤에 붙어 /modelattribute?name=req&age=1&password=pass&email=naver 형태의 query string이 된다. 테스트 실행 결과는 ModelAttributeDto{name='req', age='1', password='pass', email='naver'}로 데이터가 잘 바인딩된다.

 

// Controller.java

@PostMapping("/modelattribute") 
public ResponseEntity<ModelAttributeDto> testModelAttribute(@ModelAttribute ModelAttributeDto dto) {
    return ResponseEntity.ok(dto);
}
// ControllerTest.java

@Test
void modelAttribute() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    ModelAttributeDto modelAttributeDto = new ModelAttributeDto("req", 1L, "pass", "email");
    String requestBody = objectMapper.writeValueAsString(modelAttributeDto);

    mockMvc.perform(post("/modelattribute")
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(requestBody))
            .andExpect(status().isOk())
            .andExpect(jsonPath("name").value("req"))
            .andExpect(jsonPath("age").value("1"))
            .andExpect(jsonPath("password").value("pass"))
            .andExpect(jsonPath("email").value("email"));
}

Post 요청 테스트를 해보자. 이 테스트를 실행하면 실패한다. @ModelAttribute는 Form 형식의 HTTP 요청 본문 데이터만을 인식해 매핑하지만, JSON 형태의 데이터를 전송하고 있다. 데이터가 바인딩되지 않거나 415 Unsupported Media Type 에러가 발생한다.

 

// ControllerTest.java

mockMvc.perform(post("/modelattribute")
        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        .content("name=req&age=1&password=pass&email=naver"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("name").value("req"))
        .andExpect(jsonPath("pass").value("pass"))
        //...

이와 같이 contentType을 x-www-form-url-encoded로 요청 본문 내용을 Form 형식으로 보내도록 테스트를 수정하면 테스트 실행 결과로 ModelAttributeDto{name='req', age=1, password='pass', email='naver'}로 데이터가 잘 바인딩됨을 확인할 수 있다.

1) 생성자가 없을 때는 setter를

@RequestBody 예제처럼 필드에 접근해 데이터를 바인딩할 수 있는 ModelAttributeDto의 생성자를 삭제해보자.

// ModelAttributeDto.java

// @AllArgsConstructor
@Getter
public class ModelAttributeDto {

    private String name;
    private long age;
    private String password;
    private String email;
}

 

ModelAttributeDto{name='null', age=0, password='null', email='null'}가 출력된다. Post 요청으로 HTTP 파라미터는 정상적으로 보냈지만, Controller에서 데이터를 ModelAttributeDto에 바인딩하지 못하고 있다.

그럼 ModelAttributeDto에 setter 메서드를 추가하고 테스트를 실행하면, 테스트는 생성자가 있을 때처럼 성공하게 됩니다.

2) conclusion

- @ModelAttribute를 사용하면 HTTP 파라미터 데이터를 Java 객체에 매핑한다.

따라서, 객체의 필드에 접근해 데이터를 바인딩할 수 있는 생성자나 setter 메서드가 필요하다.

- query string 및 form 형식이 아닌 데이터는 처리할 수 없다.

 

 참고 

https://tecoble.techcourse.co.kr/post/2021-05-11-requestbody-modelattribute/

 

@RequestBody vs @ModelAttribute

1. @RequestBody와 @ModelAttribute Controller.java @RequestBody와 @ModelAttribute는 클라이언트 측에서 보낸 데이터를 Java…

tecoble.techcourse.co.kr

https://jenkov.com/tutorials/java-json/jackson-objectmapper.html#how-jackson-objectmapper-matches-json-fields-to-java-fields

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

@ModelAttribute

1. Method Level

@ModelAttribute
public void addAttributes(Model model) {
    model.addAttribute("msg", "Welcome to the Netherlands!");
}

하나 이상의 속성을 Model에 추가하고 싶을 때 메서드 레벨에서 @ModelAttribute를 추가해준다. 일반적으로 Spring MVC는 요청 핸들러 메서드를 호출하기 전에 항상 해당 메서드를 먼저 호출한다. 즉, @RequestMapping 어노테이션이 달린 컨트롤러 메서드가 호출되기 전에 @ModelAttribute 어노테이션이 달린 메서드가 호출된다. 시퀀스의 논리는 컨트롤러 메서드 내에서 처리가 시작되기 전에 모델 개체를 만들어야한다는 것이다.

 

2. Method Argument Level

@ModelAttribute는 여러 곳에 있는 단순 데이터 타입을 복합 타입 객체로 받아오거나 해당 객체를 새로 만들 때 사용할 수 있다.

여러 곳은 URI path, 요청 매개변수, 세션 등을 의미한다.

복합 객체에 데이터 바인딩을 하는 방법은 @RequestParameter로만 전달되어야하는 것은 아니다.

유연하게 여러 데이터를 하나의 복합 타입 객체로 받아올 수 있다.

 

 

만약 값을 바인딩할 수 없는 경우라면 BindException이 발생하고 400 에러가 발생한다.

만약 바인딩 에러를 직접 다루고 싶은 경우라면 @ModelAttribute가 붙은 메서드에 BindingResult 파라미터를 추가하면 된다.

@Controller
@RequestMapping("/event")
public class SampleController {
    
    @PostMapping("/names/{name}")
    @ResponseBody
    public Event getEvent(@ModelAttribute Event event, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            bindingResult.getAllErrors().forEach( c -> {
                System.out.println(c.toString());
            });
        }
        
        return event;
    }
}

 

바인딩 이후 검증 작업을 추가로 진행하고 싶다면 @Valid 또는 @Validated 어노테이션을 사용할 수 있다.

// Event.java
@Getter
@Setter
public class Event {

    private Long id;
    private String name;
    
    @Min(0)
    private Integer limit;
}

// SampleController.java
@Controller
@RequestMapping("/event")
public class SampleController {
    
    @PostMapping("/names/{name}")
    @ResponseBody
    public Event getEvent(@Valid @ModelAttribute Event event, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            bindingResult.getAllErrors().forEach( c -> {
                System.out.println(c.toString());
            });
        }
        
        return event;
    }
}

@Validated

스프링 MVC 핸들러 메서드 아규먼트에 사용할 수 있으며, Validation group 이라는 힌트를 사용할 수 있다.

@Valid 어노테이션에는 그룹을 지정할 방법이 없지만 @Validated는 스프링이 제공하는 어노테이션으로 그룹 클래스를 설정할 수 있다.

 

 참고 

https://www.baeldung.com/spring-mvc-and-the-modelattribute-annotation

https://minkukjo.github.io/framework/2020/04/13/Spring-85/

 

Spring MVC 핸들러 메소드 @ModelAttribute

Spring Web MVC

minkukjo.github.io

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Lists in Thymelead Example

예제를 위해 Book.java 을 생성한다.

@Getter
@Setter
public class Book {

    private Long id;
    private String title;
    private String author;
}

 

Displaying List Elements

BookController.java

@GetMapping("/all")
public String showAll(Model model) {
    model.addAttribute("books", bookService.findAll());
    return "books/allBooks";
}

books/allBooks.html

<table>
  <thead>
    <tr>
      <td>Title</td>
      <td>Author</td>
    </tr>
  </thead>
    <tr th:if="${books.empty}">
      <td colspan="2">No Books Avaliable</td>
    </tr>
    <tr th:each="book : ${books}">
      <td th:text="${book.title}">Title</td>
      <td th:text="${book.author}">Author</td>
    </tr>
  <tbody>
  </tbody>
</table>

th:each 속성을 사용해서 list의 iterate 문을 사용할 수 있다.

Binding a List Using Selection Expression

@AllArgsConstructor
@Getter
@Setter
public class BooksCreationDto {
    
    private List<Book> books;
    
    public void addBook(Book book) {
        this.books.add(book);
    }
}

controller에서 List 객체를 전송하기 위해선 List 객체를 그대로 사용할 수 없다. 위와 같이 wrapper 객체를 만들어주어야 한다.

 

@GetMapping("/create")
public String showCreateForm(Model model) {
    BooksCreationDto booksForm = new BooksCreationDto();

    for (int i = 1; i <= 3; i++) {
        booksForm.addBook(new Book());
    }

    model.addAttribute("form", booksForm);
    return "books/createBooksForm";
}

Model 속성에 3개의 빈 Book 객체를 생성하여 추가하였다.

 

<form action="#" th:action="@{/books/save}" th:object="${form}"method="post">
    <fieldset>
        <input type="submit" id="submitButton" th:value="Save">
        <input type="reset" id="resetButton" name="reset" th:value="Reset"/>
        <table>
            <thead>
                <tr>
                    <th> Title</th>
                    <th> Author</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="book, itemStat : *{books}">
                    <td><input th:field="*{books[__${itemStat.index}__].title}" /></td>
                    <td><input th:field="*{books[__${itemStat.index}__].author}" /></td>
                </tr>
            </tbody>
        </table>
    </fieldset>
</form>

Thymeleaf를 실행하면 3개의 빈 Book 객체가 있을 것이다.

th:object="${form}"

form에서 submit을 할 때, form의 데이터가 th:object에 설정해준 객체로 받아진다.

<tr th:each="book, itemStat : *{books}">

각각 필드들을 매핑을 해주는 역할을 한다. 설정해 준 값으로, th:object에 설정해 준 객체의 내부와 매칭해준다.

@PostMapping("/save")
public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) {
    bookService.saveAll(form.getBooks());

    model.addAttribute("books", bookService.findAll());
    return "redirect:/books/all";
}

@ModelAttribute 어노테이션을 활용하여 객체를 가져올 수 있다.

Book list를 저장하고 list 화면으로 redirect 해주면 다음과 같은 화면을 얻을 수 있다.

Binding a List Using Variable Expression

@GetMapping("/edit")
public String showEditForm(Model model) {
    List<Book> books = new ArrayList<>();
    bookService.findAll().iterator().forEachRemaining(books::add);

    model.addAttribute("form", new BooksCreationDto(books));
    return "books/editBooksForm";
}
<tr th:each="book, itemStat : ${form.books}">
    <td>
        <input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"/>
    </td>
    <td>
        <input th:name="|books[${itemStat.index}].author|" th:value="${book.getAuthor()}"/>
    </td>
</tr>

적절하게 data를 submit하기 위해선 name과 value값을 지정해주어야 한다. edit 해주기 위해선 books.id도 전달해주어야 하는데, hidden 속성을 통해 보이지 않게 전달하고 있다.

 

출처

https://www.baeldung.com/thymeleaf-list

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Express Example

적당한 위치에 애플리케이션 디렉터리를 생성하고 npm init을 실행한다. 일단 기본 설정으로 package.json을 생성한다.

$ mkdir express-mysql-example
$ cd express-mysql-example
$ npm init --yes

mysql 모듈을 설치한다.

$ npm install mysql

package.json을 아래와 같이 수정한다.

{
  "name": "express-mysql-example",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index"
  },
  "dependencies": {
    "mysql": "^2.18.1"
  }
}

Node.js와 MySQL 연동

index.js를 아래와 같이 변경한다. 

createConnection 메서드의 인자로 전달되는 객체에 자신의 데이터베이스 정보(유저명과 패스워드 등)를 입력해야 한다. 

const mysql = require('mysql');
const connection = mysql.createConnection({
    host	: 'localhost',
    user	: 'jy.jeon',
    password: 'password',
    database: 'my_db'
});

connection.connect();

connection.query('SELECT * FROM Users', (error, rows, fields) => {
    if (error) throw error;
    console.log('User info is: ', rows);
});

connection.end();

콘솔에 아래와 같은 결과가 출력되면 성공이다.

User info is:  [ RowDataPacket { id: 'nero', password: '1234' } ]

만약 ‘ER_NOT_SUPPORTED_AUTH_MODE’ 에러가 발생하면 아래 sql을 실행하고 다시 접속해보자.

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
FLUSH PRIVILEGES;

 

npm start로 실행한다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !