스프링 - 지인 정보 관리 시스템

|

패스트캠퍼스 스프링 강의 정리

JPA: Persistence 레이어 Controller: Presentation 레이어

02. HelloWorldController 생성

모름지기 모든 시작은 Hello World 찍기 인거 같다. 메인 패키지에서 하위에 controller 패키지를 만들어 준 후 HelloWorldController 자바 파일을 생성해서 입력해준다. @RestController라는 어노테이션은 스프링이 해당 클래스가 컨트롤러임을 알려준다. @GetMapping은 HTTP Method 중 GET임을 알려준다. 이후 커맨드에서 ./gradlew bootRun 입력해주면 웹 서버가 실행되고, http://localhost:8080/api/helloWorld를 입력해주면 HelloWorld가 표시된다.

@RestController
public class HelloWorldController {

    @GetMapping(value = "/api/helloWorld")
    public String helloWorld() {
        return "HelloWorld";
    }

IntelliJ 단축키 중 Command + B를 하면 클래스의 위치를 찾아서 보여준다.

웹 브라우저에서 말고 IntelliJ에서도 확인이 가능한데, 테스트 패키지에서 hello.http라는 파일을 만들어 준 후, 다음처럼 입력해 보자

GET http:localhost:8080/api/helloWorld

이후 실행을 해보면 리턴 값을 돌려받는 걸 확인할 수 있다.

03. MockMvc 테스트 만들기

Command + Shift + t를 입력하면 테스트를 만들 수 있는 단축키이다. ```java package com.fastcampus.javaallinone.project3.mycontact.controller;

import org.junit.jupiter.api.Test; // Junit5꺼 사용 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest class HelloWorldControllerTest { @Autowired // Spring Context에 Bean 주입 private HelloWorldController helloWorldController;

private MockMvc mockMvc;

@Test
void helloWorld() { //        System.out.println("test");
    System.out.println(helloWorldController.helloWorld());
}

@Test
void mockMvcTest() throws Exception {
    mockMvc = MockMvcBuilders.standaloneSetup(helloWorldController).build();

    mockMvc.perform(
            MockMvcRequestBuilders.get("/api/helloWorld"))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("HelloWorld"));
} } ```

JPA

JPA는 ORM 표준 인터페이스이다. 이 인터페이스를 활용하면 내가 어떠한 DB를 사용해도 그에 맞는 쿼리로 처리해주어 아주 좋다. build.gradle에 의존성을 추가 해준다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'

h2는 초경량 메모리 DB이다. 테스트나 캐싱용으로 많이 사용된다.

객체를 디비에 저장하기 위해서 Repository를 만들어 준다. 아래와 같이 JPaRepository를 상속받는 인터페이스를 만들어 주면 된다. 제너릭으로는 엔티티와 primary key의 값 타입을 넣어주면 된다.

package com.fastcampus.javaallinone.project3.mycontact.repository;

import com.fastcampus.javaallinone.project3.mycontact.domain.Person;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PersonRepository extends JpaRepository<Person, Long> {
}

이후 테스트는 아래처럼 해보면 된다.

package com.fastcampus.javaallinone.project3.mycontact.repository;

import com.fastcampus.javaallinone.project3.mycontact.domain.Person;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class PersonRepositoryTest {

    @Autowired
    private PersonRepository personRepository;

    @Test
    void crud() {
        Person person = new Person();
        person.setName("seokju");
        person.setAge(10);

        personRepository.save(person);

//        System.out.println(personRepository.findAll());

        List<Person> people = personRepository.findAll();

        Assertions.assertThat(people.size()).isEqualTo(1);
        Assertions.assertThat(people.get(0).getName()).isEqualTo("seokju");
        Assertions.assertThat(people.get(0).getAge()).isEqualTo(10);
    }
}

Lombok - 1

IntelliJ에서 lombok을 사용하기 위해서는 환결설정에서 Build, Execution, Deployment -> Compiler -> Annotaion Processors에서 Enable annotaion processing을 체크해주어야 한다.

lombok을 사용하면 Getter, Setter, ToString을 편안하게 이용할 수 있다. 기존에 우리가 멤버변수를 만들고 나서는 일일히 Getter, Setter함수를 만들어 주어야 했다. 물론 IDE에서 단축키를 통해 편하게 만들 수도 있지만 이것 또한 일이다. 그런 불편함을 lombok이 해결해준다.

@Entity
@Getter
@Setter
public class Person {
    @Id
    @GeneratedValue // 자동 생성
    private Long id;

    private String name;

    private int age;

    private String hobby;

    private String bloodType;

    private String address;

멤버 변수마다 @Getter, @Setter 넣어주는 방법도 있지만, 위처럼 클래스 위에 입력하면 모든 변수가 적용된다.

다음은 ToString인데, ToString 또한 만약 변수가 추가되어 새로 넣어야 하면 이것 또한 노가다이다. lombok은 ToString도 자동으로 처리해준다.

@Entity
@Getter
@Setter
@ToString
public class Person {
    @Id
    @GeneratedValue // 자동 생성
    private Long id;

    private String name;

    private int age;

    private String hobby;

    private String bloodType;

    private String address;

    private String phoneNumber;

@ToString 어노테이션을 넣어주면 클래스 내의 멤버변수들로 적절하게 만들어 준다. 만약 전화번호는 개인정보라 노출되지 않길 바란다면 예외로 둘 수 있다. 아래와 같이 두가지 방법이 있다.

// 1번 - 클래스 위의 ToString에 내용 추가
@ToString(exclude="phoneNumber")

// 2번 - 멤버변수 위에 Exclude 어노테이션을 추가
@ToString.Exclude
private String phoneNumber;

lombok - 2

자바 클래스는 기본적으로 매개변수가 없는 생성자를 가지고 있다. 생성자 또한 lombok을 통해 기본 생성자와 전체 매개변수를 가진 생성자, 또는 특정 생성자를 만들 수 있다.

  • NoArgsConstructor: 매개 변수 없는 생성자
  • AllArgsConstructor: 전체 매개변수 가진 생성자
  • RequiredArgsConstructor: 특정 매개변수 생정자이다. 이거 같은 경우에는 멤버 변수 위에 @NonNull 어노테이션을 달아준다.
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
public class Person {
    @Id
    @GeneratedValue // 자동 생성
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private int age;

    private String hobby;

또한 HashCode와 equlas 메소드도 오버라이딩 해서 재정의를 많이 해주는데 이 또한 lombok에서 제공해준다. 위에 @EqualsAndHashCode 어노테이션을 달아주면 필드에 맞게 값을 맞춰 줄 것이다.

@EqualsAndHashCode
public class Person{}

마지막으로 @Getter, @Setter, ToString, EqualsAndHashCode 등을 한번에 제공하는 어노테이션이 있는데 이것이 @Data 어노테이션이다. 기존꺼를 다 지우고 @Data만 적으면 동일하게 작동한다.

@Entity
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
public class Person {
    @Id
    @GeneratedValue // 자동 생성
    private Long id;

    @NonNull
    private String name;

    @NonNull
    private Integer age;

    private String hobby;

    @NonNull
    private String bloodType;

    private String address;

    private LocalDate birthday;

    private String job;

    @ToString.Exclude
    private String phoneNumber;
}

JPA Relation

테이블 간 관계를 맺기 위해서 @OneToOne, @OneToMany 같은 어노테이션을 설정해야한다. 아래의 예제는 Block 엔티티를 연결하는 방법이다. CASACADETYPE과 orphanRemoval 등을 설정해주어야 삭제 시 같이 연쇄적으로 삭제되거나 업데이트 될 것이다. 자세한 내용은 추후에 정리해봐야겠다. FetchType EAGER는 조인을 통해 갖고오는 형태이고, LAZY는 실제 사용할 때 SELECT해온다.

public class Person {
    @OneToOne(cascade = {CascadeType.ALL}, orphanRemoval = true)
    @ToString.Exclude
    private Block block;
}

JPA Query Method

레포지토리의 findAll과 stream을 적절히 조합하면 내가 원하는 데이터를 찾을 수 있지만 이것은 모든 데이터를 갖고오는 비효율을 초래한다. 따라서 JPA의 Query Method를 이용해서 적절한 값을 갖고 올 수 있도록 하는 것이 좋다. 메소드의 구문은 대략 이러하다.

findById

sql로 따지면 find는 select, by는 where, Id가 이제 컬럼명이 된다. repository에 직접 구현을 해보자.

public interface PersonRepository extends JpaRepository<Person, Long> {
    // 이름으로 검색
    List<Person> findByName(String name);

    // Block이 null인 것들 검색
    List<Person> findByBlockIsNull();

    // 혈액형으로 검색
    List<Person> findByBloodType(String bloodType);

    // 날짜로 검색 Between의 경우 두 인자 사이에 포함된 값들을 검색한다.
    List<Person> findByBirthdayBetween(LocalDate startDate, LocalDate endDate);
}

JPA @Query

위 Query Method를 사용해서 8월달이 생일인 친구들을 갖고오는 테스트를 만들어 보겠다.

@Test
void findByBirthdayBetween() {
    givenPerson("seokju", 10, "A", LocalDate.of(1991, 8, 15));
    givenPerson("david", 9, "B", LocalDate.of(1992, 7, 10));
    givenPerson("dennis", 8, "O", LocalDate.of(1993, 1, 5));
    givenPerson("sophia", 7, "AB", LocalDate.of(1994, 6, 30));
    givenPerson("benny", 6, "A", LocalDate.of(1995, 8, 30));

    List<Person> result = personRepository.findByBirthdayBetween(LocalDate.of(1991, 8, 1), LocalDate.of(1995, 8, 31));

    result.forEach(System.out::println);
}

private void givenPerson(String name, int age, String bloodType) {
    givenPerson(name, age, bloodType, null);
}

private void givenPerson(String name, int age, String bloodType, LocalDate birthday) {
    Person person = new Person(name, age, bloodType);
    person.setBirthday(birthday);
    personRepository.save(person);
}

List result = personRepository.findByBirthdayBetween(LocalDate.of(1991, 8, 1), LocalDate.of(1995, 8, 31));

이렇게만 보면 8월달 생일인 친구들을 갖고오는거 갖지만 Between이 무엇인가. 그 사이에 있는 데이터를 다 갖고오는 것이기 때문에 위에서 만든 친구들이 전부 갖고 와 질 것이다. 이렇게 되면 우리가 의도했던거와는 전혀 다른 방향으로 바뀌는 것이다. 이것을 @Embedded, @Embeddable, @Valid, @Query 어노테이션으로 해결해보자.

@Valid 어노테이션을 사용하려면 build.gradle dependencies에 implementation ‘org.springframework.boot:spring-boot-starter-validation’를 추가해주자.

먼저 domain/dto/Birthday.java를 생성하여 Person의 birthday를 조금 수정해 준다.

@Embeddable // Entity에 속해있는 DTO
@Data
@NoArgsConstructor
public class Birthday {
    private int yearOfBirthday;

    @Min(1)
    @Max(12)
    private int monthOfBirthday;

    @Min(1)
    @Max(31)
    private int dayOfBirthday;

    public Birthday(LocalDate birthday) {
        this.yearOfBirthday = birthday.getYear();
        this.monthOfBirthday = birthday.getMonthValue();
        this.dayOfBirthday = birthday.getDayOfMonth();
    }
}

이후 Person의 birthday를 다음과 같이 수정해준다.

@Valid
@Embedded
private Birthday birthday;

이렇게 하고 PersonRepository에 다음을 추가해준다. @Query어노테이션을 활용하면 우리가 JPQL이라 해서 Java와 Sql문을 혼재해서 사용할 수 있게 해주는 역할을 한다. 이렇게 하여 월에 해당하는 조건을 추가하여 해당 월에 맞는 데이터를 추출할 수 있게 해준다. native=true를 사용하면 실제 sql문을 사용하게 된다. ?1이나 ?2는 메소드의 매개변수 순서를 가르키는 것이고, 추가로 @Param 어노테이션을 활용하면 ?숫자 대신 지정한 이름으로 쓸 수 있다.

//    @Query(value = "select person from Person person where person.birthday.monthOfBirthday = ?1 and person.birthday.dayOfBirthday=?2") // jpql (Entity 기반 퀀리), ?1은 첫번째 인자
// native = true는 실제 SQl로
//    @Query(value = "select * from person where month_of_birthday = :monthOfBirthday and day_of_birthday =:dayOfBirthday", nativeQuery = true)
@Query(value = "select person from Person person where person.birthday.monthOfBirthday = :monthOfBirthday")
List<Person> findByMonthOfBirthday(@Param("monthOfBirthday") int monthOfBirthday);
//    List<Person> findByMonthOfBirthday(@Param("monthOfBirthday") int monthOfBirthday, @Param("dayOfBirthday") int dayOfBirthday);

JPA data.sql

test시 data.sql을 만들어 사용하면 편하게 데이터를 삽입하여 관리할 수 있다.

spring2.5부터는 위 내용을 이용하려면 applicaion.yml 같은 파일에 다음을 추가해준다. defer-datasource-initialization: true

test디렉토리에 resources 디렉토리를 만든 후 data.sql을 만든다.

insert into person(`id`, `name`, `age`, `blood_type`, `year_of_birthday`, `month_of_birthday`, `day_of_birthday`) values (1, 'martin', 10, 'A', 1991, 8, 15);
insert into person(`id`, `name`, `age`, `blood_type`, `year_of_birthday`, `month_of_birthday`, `day_of_birthday`) values (2, 'david', 9, 'B', 1992, 7, 21);
insert into person(`id`, `name`, `age`, `blood_type`, `year_of_birthday`, `month_of_birthday`, `day_of_birthday`) values (3, 'dennis', 8, 'O', 1993, 10, 15);
insert into person(`id`, `name`, `age`, `blood_type`, `year_of_birthday`, `month_of_birthday`, `day_of_birthday`) values (4, 'sophia', 7, 'AB', 1994, 8, 31);
insert into person(`id`, `name`, `age`, `blood_type`, `year_of_birthday`, `month_of_birthday`, `day_of_birthday`) values (5, 'benny', 6, 'A', 1995, 12, 23);

insert into block(`id`, `name`) values (1, 'dennis');
insert into block(`id`, `name`) values (2, 'sophia');

update person set block_id = 1 where id = 3;
update person set block_id = 2 where id = 4

이런식으로 하여 repository에서 굳이 우리가 객체 생성없이 db에 있는 내용으로 실습할 수 있게 된다.

Controller

RESTful하게 사용하기 위한 어노테이션으로 GET과 관련된 어노테이션은 아래와 같다.

  • @GetMapping
  • @PathVariable

@RequestMapping(value = “url명”)을 클래스 위에 두면 전체적인 url로 해당 내용의 root url이 된다 보면 된다. @RestController는 Rest컨트롤러임을 알려준다. @GetMapping은 GET메소드로 오는 것에 대한 어노테이션이다. 메소드에서 @GetMapping(“/{id}”)와 매개변수 쪽에 @PathVariable을 사용하여 매칭시켜준다. 일반적인 GET URL양식은 http:url/api/person?id=1형태로 끝에 물음표와 값이 들어가는데 REST적으로 사용하면 http://url/api/person/1 이런식으로 사용하게 된다.

@RequestMapping(value = "/api/person")
@RestController
public class PersonController {
    @Autowired
    private PersonService personService;

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        return personService.getPerson(id);
    }
}

POST

POST는 다음과 같이 입력해보면 된다. @PostMapping 어노테이션을 걸어주면 POST 처리가 된다. 보통 REST 방식의 처리는 Request body안의 내용가지고 처리하기에 매개변수란에 @RequestBody 어노테이션을 걸어주면 된다. 그리고 처리가 정상적으로 완려되면 200 코드를 보내주는데, @ResponseStatus어노테이션을 활용하면 내가 원하는 코드로 보낼 수 있다.

@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 Response
public void postPerson(@RequestBody Person person) { // 어노테이션이 없으면 @RequestParam으로 처리
    personService.put(person);

    log.info("person -> {}", personRepository.findAll());
}

Put

Shift + fn + f6을 누르면 메소드내의 이름 바꿀 때 좋다.

  • @PutMapping

수정하는 서비스 내용은 다음과 같다.
근데 이런식으로 하면 내가 업데이트 정보에서 누락된게 있으면 null로 되어버리는 경우도 있기 때문에 좋지 않은 코드이다. 
@Transactional
public void modify(Long id, PersonDto personDto ) {
    Person personAtDb = personRepository.findById(id).orElseThrow(() -> new RuntimeException("아이디가 존재하지 않습니다."));

    if (!personAtDb.getName().equals(personDto.getName())) {
        throw new RuntimeException("이름이 다릅니다.");
    }

    personAtDb.setName(personDto.getName());
    personAtDb.setPhoneNumber(personDto.getPhoneNumber());
    personAtDb.setJob(personDto.getJob());

    if (personDto.getBirthday() != null) {
        personAtDb.setBirthday(new Birthday(personDto.getBirthday()));
    }
    personAtDb.setAddress(personDto.getAddress());
    personAtDb.setBloodType(personDto.getBloodType());
    personAtDb.setHobby(personDto.getHobby());
    personAtDb.setAge(personDto.getAge());

    personRepository.save(personAtDb);
}

Person DTO를 만들어 주자

package com.fastcampus.javaallinone.project3.mycontact.controller.dto;

import lombok.Data;

import java.time.LocalDate;

@Data
public class PersonDto {
    private String name;
    private int age;
    private String hobby;
    private String bloodType;
    private String address;
    private LocalDate birthday;
    private String job;
    private String  phoneNumber;
}

그러고 Person Entity에서 유효성 검사라 해야하나 암튼 set 메서드를 만들어 주자.

public void set(PersonDto personDto) {
    if (personDto.getAge() != 0) { // personDTO에서 age를 primitive형으로 해서 가능
        this.setAge(personDto.getAge());
    }

    if (!StringUtils.isEmpty(personDto.getHobby())) {
        this.setHobby(personDto.getHobby());
    }

    if (!StringUtils.isEmpty(personDto.getBloodType())) {
        this.setBloodType(personDto.getBloodType());
    }

    if (!StringUtils.isEmpty(personDto.getAddress())) {
        this.setAddress(personDto.getAddress());
    }

    if (!StringUtils.isEmpty(personDto.getJob())) {
        this.setAddress(personDto.getJob());
    }

    if (!StringUtils.isEmpty(personDto.getPhoneNumber())) {
        this.setPhoneNumber(personDto.getPhoneNumber());
    }
}

Patch

데이터의 일부만 수정할 때 쓰이는 메소드이다.


// Controller
@PatchMapping("/{id}") // 일부 리소스만 변경
public void modifyPerson(@PathVariable Long id, String name) {
    personService.modify(id, name);

    log.info("person -> {}", personRepository.findAll());
}

// Services
@Transactional
public void modify(Long id, String name) {
    Person person = personRepository.findById(id).orElseThrow(() -> new RuntimeException("아이디가 존재하지 않습니다."));

    person.setName(name);

    personRepository.save(person);
}

// Test
@Test
void modifyName() throws Exception {
    mockMvc = MockMvcBuilders.standaloneSetup(personController).build();

    mockMvc.perform(
            MockMvcRequestBuilders.patch("/api/person/1")
            .param("name", "martin22"))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk());
}

DELETE

// Controller
@DeleteMapping("/{id}")
public void deletePerson(@PathVariable Long id) {
    personService.delete(id);

    log.info("person -> {}", personRepository.findAll());
}

// Services
@Transactional
public void delete(Long id) {
//        Person person = personRepository.findById(id).orElseThrow(() -> new RuntimeException("아이디가 존재하지 않습니다."));

//        personRepository.delete(person);
    personRepository.deleteById(id);
}

@Test
void deletePerson() throws Exception {
    mockMvc.perform(
            MockMvcRequestBuilders.delete("/api/person/1"))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk());
}

이런식으로 지울 수도 있지만, 이런 경우 데이터 복구하기 어려워지기에 플래그를 활용하여 소프트하게 삭제를 한다. @Where(clause = “deleted = false”)는 findAll같은거 할 때 where절을 추가할 때 사용하는 것이다. @ColumnDefault(“0”)는 false로 저장할 때 사용되는 부분이다. 변경

// Entity
@Where(clause = "deleted = false")
public class Person {
    @ColumnDefault("0")
    private boolean deleted;
}

@Transactional
public void delete(Long id) {
    Person person = personRepository.findById(id).orElseThrow(() -> new RuntimeException("아이디가 존재하지 않습니다."));

    person.setDeleted(true);

    personRepository.save(person);
}

Refactoring

테스트를 잘 만들면 변경내용이 적용되는 범위를 금방 찾고, 사이드이펙트를 체크해볼 수 있다. 오류는 컴파일 단계, 테스트 단계, 런타임 단계 순으로 빠르게 발생할 수 있도록 해야 좋은 코드이다.

Controller Test

Delete Controller에서 기존까지는 void로 반환했기에 삭제가 제대로 되었는지 알 수 없었다. 이를 해결하기 위한 방법으로는 크게 세가지 정도가 있다.

  1. boolean 형태로 반환하기. 반환형을 void에서 boolean타입으로 지정하고 return true로 바꾼다. 그런데 만약 우리가 삭제하는 서비스 코드를 지워도 무조건 true로 반환하기 때문에 좋진 않다.

  2. 삭제됐는지 확인하여 true, false 반환
    @DeleteMapping("/{id}")
    public boolean deletePerson(@PathVariable Long id) {
     personService.delete(id);
    
     log.info("person -> {}", personRepository.findAll());
    
     return personRepository.findPeopleDeleted().stream().anyMatch(person -> person.getId().equals(id));
    }
    
  3. 테스트에서 assert들을 활용하여 확인한다.

assertAll은 jUnit5의 기능인데, 한번에 assert들을 확인할 수 있다.

@Test
void modifyPerson() throws Exception {
    PersonDto dto = PersonDto.of("martin", "programming", "seoul", LocalDate.now(), "programmer", "010-1111-2222");

    mockMvc.perform(
            MockMvcRequestBuilders.put("/api/person/1")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content(toJsonString(dto)))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isOk());

    Person result = personRepository.findById(1L).get();

    assertAll(
            () -> assertThat(result.getName()).isEqualTo("martin"),
            () -> assertThat(result.getHobby()).isEqualTo("programming"),
            () -> assertThat(result.getAddress()).isEqualTo("seoul"),
            () -> assertThat(result.getBirthday()).isEqualTo(Birthday.of(LocalDate.now())),
            () -> assertThat(result.getJob()).isEqualTo("programmer"),
            () -> assertThat(result.getPhoneNumber()).isEqualTo("010-1111-2222")
    );
}

03

Requestbody 파라미터로 엔티티 객체를 받는 것은 안전하지 못한 방법이다. ID나 Delete관련해서 사용자가 보내면 안되기 때문이다. 그래서 Dto 객체를 파라미터로 지정하여 값을 받고 서버에서 엔티티 객체를 만들도록 하는 것이 좋다.

// Controller
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 Response
public void postPerson(@RequestBody PersonDto personDto) { // 어노테이션이 없으면 @RequestParam으로 처리
    personService.put(personDto);
}


// Service
@Transactional
public void put(PersonDto personDto) {
    Person person = new Person();
    person.set(personDto);
    person.setName(personDto.getName());
    personRepository.save(person);
}

// Test
@Test
void postPerson() throws Exception {
    PersonDto dto = PersonDto.of("martin", "programming", "seoul", LocalDate.now(), "programmer", "010-1111-2222");
    mockMvc.perform(
            MockMvcRequestBuilders.post("/api/person")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content(toJsonString(dto)))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isCreated());

    Person result = personRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).get(0);
    assertAll(
            () -> assertThat(result.getName()).isEqualTo("martin"),
            () -> assertThat(result.getHobby()).isEqualTo("programming"),
            () -> assertThat(result.getAddress()).isEqualTo("seoul"),
            () -> assertThat(result.getBirthday()).isEqualTo(Birthday.of(LocalDate.now())),
            () -> assertThat(result.getJob()).isEqualTo("programmer"),
            () -> assertThat(result.getPhoneNumber()).isEqualTo("010-1111-2222")
    );
}

Repository Test

@Transactional
@SpringBootTest
class PersonRepositoryTest {

    @Autowired
    private PersonRepository personRepository;

    @Test
    void findByName() {
        List<Person> people = personRepository.findByName("tony");
        Assertions.assertThat(people.size()).isEqualTo(1);

        Person person = people.get(0);
        assertAll(
                () -> Assertions.assertThat(person.getName()).isEqualTo("tony"),
                () -> Assertions.assertThat(person.getHobby()).isEqualTo("reading"),
                () -> Assertions.assertThat(person.getAddress()).isEqualTo("서울"),
                () -> Assertions.assertThat(person.getBirthday()).isEqualTo(Birthday.of(LocalDate.of(1991, 7, 10))),
                () -> Assertions.assertThat(person.getJob()).isEqualTo("officer"),
                () -> Assertions.assertThat(person.getPhoneNumber()).isEqualTo("010-2222-5555"),
                () -> Assertions.assertThat(person.isDeleted()).isEqualTo(false)
        );
    }

    @Test
    void findByNameIfDelted() {
        List<Person> people = personRepository.findByName("andrew");
        Assertions.assertThat(people.size()).isEqualTo(0);
    }

    @Test
    void findByMonthOfBirthday() {
        List<Person> people = personRepository.findByMonthOfBirthday(7);

        Assertions.assertThat(people.size()).isEqualTo(2);
        assertAll(
                () -> Assertions.assertThat(people.get(0).getName()).isEqualTo("david"),
                () -> Assertions.assertThat(people.get(1).getName()).isEqualTo("tony")
        );
    }

    @Test
    void findPeopleDeleted() {
        List<Person> people = personRepository.findPeopleDeleted();

        Assertions.assertThat(people.size()).isEqualTo(1);
        Assertions.assertThat(people.get(0).getName()).isEqualTo("andrew");

    }
}

Service Test

mockito 테스트로 테스트를 해보자. build.gradle에 의존성을 추가해주자.

testImplementation 'org.mockito:mockito-junit-jupiter'

PersonServiceTest를 아래와 바꿔서 실행해본다.

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {
    @InjectMocks
    private PersonService personService;
    @Mock
    private PersonRepository personRepository;

    @Test
    void getPeopleByName() {
        when(personRepository.findByName("martin"))
                .thenReturn(Lists.newArrayList(new Person("martin")));

        List<Person> result = personService.getPeopleByName("martin");

        Assertions.assertThat(result.size()).isEqualTo(1);
        Assertions.assertThat(result.get(0).getName()).isEqualTo("martin");
    }

    @Test
    void getPerson() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.of(new Person("martin")));

        Person person = personService.getPerson(1L);

        Assertions.assertThat(person.getName()).isEqualTo("martin");
    }

    @Test
    void getPersonIfNotFound() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.empty());

        Person person = personService.getPerson(1L);

        Assertions.assertThat(person).isNull();
    }

    @Test
    void put() {
        PersonDto dto = PersonDto.of("martin", "programming", "seoul", LocalDate.now(), "programmer", "010-1111-2222");

        personService.put(mockPersonDto());

        verify(personRepository, times(1)).save(argThat(new IsPersonWillBeInserted()));
    }

    @Test
    void modifyIfPersonNotFound() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.empty());

        assertThrows(RuntimeException.class, () -> personService.modify(1L, mockPersonDto()));
    }

    @Test
    void modifyIfNameIsDifferent() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.of(new Person("tony")));
        assertThrows(RuntimeException.class, () -> personService.modify(1L, mockPersonDto()));
    }

    @Test
    void modify() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.of(new Person("martin")));

        personService.modify(1L, mockPersonDto());

//        verify(personRepository, times(1)).save(any(Person.class)); // return void에 대한 확인
        verify(personRepository, times(1)).save(argThat(new IsPersonWillBeUpdated()));
    }

    @Test
    void modifyByNameIfPersonNotFound() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.empty());

        assertThrows(RuntimeException.class, () -> personService.modify(1L, "daniel"));
    }

    @Test
    void modifyByName() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.of(new Person("martin")));

        personService.modify(1L, "daniel");

        verify(personRepository, times(1)).save(argThat(new IsNameWillBeUpdated()));
    }

    @Test
    void deleteIfPersonNotFound() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.empty());

        assertThrows(RuntimeException.class, () -> personService.delete(1L));
    }

    @Test
    void delete() {
        when(personRepository.findById(1L))
                .thenReturn(Optional.of(new Person("martin")));
        personService.delete(1L);
        verify(personRepository, times(1)).save(argThat(new IsPersonWillBeDeleted()));
    }

    private PersonDto mockPersonDto() {
        return PersonDto.of("martin", "programming", "seoul", LocalDate.now(), "programmer", "010-1111-2222");
    }

    private static class IsPersonWillBeInserted implements ArgumentMatcher<Person> {
        @Override
        public boolean matches(Person person) {
            return equals(person.getName(), "martin")
                    && equals(person.getHobby(), "programming")
                    && equals(person.getAddress(), "seoul")
                    && equals(person.getBirthday(), Birthday.of(LocalDate.now()))
                    && equals(person.getJob(), "programmer")
                    && equals(person.getPhoneNumber(), "010-1111-2222");
        }
        private boolean equals(Object actual, Object expected) {
            return expected.equals(actual);
        }
    }

    private static class IsPersonWillBeUpdated implements ArgumentMatcher<Person> {
        @Override
        public boolean matches(Person person) {
            return equals(person.getName(), "martin")
                    && equals(person.getHobby(), "programming")
                    && equals(person.getAddress(), "seoul")
                    && equals(person.getBirthday(), Birthday.of(LocalDate.now()))
                    && equals(person.getJob(), "programmer")
                    && equals(person.getPhoneNumber(), "010-1111-2222");
        }

        private boolean equals(Object actual, Object expected) {
            return expected.equals(actual);
        }
    }

    private static class IsNameWillBeUpdated implements ArgumentMatcher<Person> {
        @Override
        public boolean matches(Person person) {
            return person.getName().equals("daniel");
        }
    }

    private static class IsPersonWillBeDeleted implements ArgumentMatcher<Person> {
        @Override
        public boolean matches(Person person) {
            return person.isDeleted();
        }
    }

Paging

많은 웹사이트들이 비단 제품 하나만 보는게 아니라 전체보기 같은 기능들을 제공한다. 그런데 정말로 서버에서 모든 데이터를 갖고오는 것은 비효율적이기 때문에 페이징 처리를 해야한다.

// Controller
@GetMapping
public Page<Person> getAll(@PageableDefault Pageable pageable) { // 기본 페이지 내용 설정
    return personService.getAll(pageable);
}

// Service
public Page<Person> getAll(Pageable pageable) {
    return personRepository.findAll(pageable);
}

// Test
@Test
void getAll() throws Exception {
    mockMvc.perform(
            MockMvcRequestBuilders.get("/api/person")
                    .param("page", "1")
                    .param("size", "2"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages").value(3)) // $ 해당 객체
            .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements").value(6))
            .andExpect(MockMvcResultMatchers.jsonPath("$.numberOfElements").value(2))
            .andExpect(MockMvcResultMatchers.jsonPath("$.content.[0].name").value("dennis"))
            .andExpect(MockMvcResultMatchers.jsonPath("$.content.[1].name").value("sophia"));
    }

검색

|

Do it 자료구조 책을 보고 정리

검색 알고리즘

데이터 집합에서 원하는 값을 가진 요소를 찾으려면 검색 알고리즘에 대해 알아야 한다. 주소록을 검색한다고 가정하면 다음 처럼 찾아 볼 수 있다.

  1. 국적이 한국인 사람을 찾는다.
  2. 나이가 21세 이상 27세 미만인 사람을 찾는다.
  3. 찾으려는 이름과 가장 비슷한 이름의 사람을 찾는다.

위처럼 무언가 검색을 할 때 특정 항목에 주목을 하게 되는데 그 항목을 ‘키’라고 한다. 위의 키값은 이러하다.

  1. 키 값과 일치하도록 지정(한국)
  2. 키 값의 구간을 지정(21세 이상 27세 미만)
  3. 키 값과 비슷하도록 지정(발음이 가장 비슷한 이름)

이러한 조합들을 논리곱이나 논리합을 사용해서 더 복합적으로도 사용할 수 있다.

검색의 종류

검색의 종류는 보통 이러하다.

  1. 배열 검색
  2. 선형 리스트 검색
  3. 이진검색트리 검색

지금 알아 볼 것은 배열 검색이다. 배열 검색은 말그대로 배열을 활용하는 기법이고, 아래의 알고리즘을 활용한다.

  1. 선형 검색: 무작위로 늘어놓은 데이터 모임에서 검색을 수행
  2. 이진 검색: 일정한 규칙으로 늘어놓은 데이터 모임에서 아주 빠른 검색을 수행
  3. 해시법: 추가, 삭제가 자주 일어나는 데이터 모임에서 아주 빠른 검색을 수행
    • 체인법: 같은 해시 값의 데이터를 선형 리스트로 연결
    • 오픈 주소법: 데이터를 위한 해시 값이 충돌할 때 재해시하는 방법

여기서 그냥 아무거나 갖다 쓰면 되지 않을까 생각하지만, 경우에 따라 적절한 알고리즘을 가져다 써야 한다. 만약에 추가, 삭제가 빈번히 일어나는 환경인데 검색속도만 빠른 알고리즘을 쓰면은 낭패가 발생하기 때문이다.

선형 검색

배열을 활용한 선형 검색을 해보도록 하겠다. 선형 검색은 배열에서 원하는 키 값을 갖는 요소를 만날 때까지 맨 앞부터 순서대로 요소를 검색한다. 그렇기에 선형 검색(linear search) 또는 순차 검색(sequential search)라고 한다. 예를 들어 아래와 같은 배열이 있다. 여기서 내가 2를 검색한다 가정해보자. 6 4 3 2 1 3 8 그러면 6부터 시작해서 4, 3 순으로 비교하고 3번 인덱스에 있는 2를 발견하면 검색을 성공하고 종료하게 된다. 만약 내가 배열에 없는 값 9를 찾는다면 배열의 끝까지 가고 종료하게 된다. 선형 검색의 종료 조건은 2개가 있고, 하나라도 성립하면 검색이 종료된다.

  1. 검색할 값을 발견하지 못하고 배열의 끝을 지나간 경우
  2. 검색할 값과 같은 요소를 발견할 경우

자바(5) - 클래스

|

객체와 객체 지향 프로그래밍

객체의 사전적 의미로는 ‘의사나 행위가 미치는 대상’이라 한다. 우리 주위에 있는 객체들론 사람, 자동차, 건물 등이 있다. 즉 눈에 보이는 사물은 모두 객체라고 볼 수 있는데, 눈에 안 보이는 것도 객체가 될 수 있다. 가령, 주문, 생산, 관리 등 행동을 나타내는 단어도 객체가 될 수 있다. 이러한 객체들에 대해서 우리는 자바로 표현할 수 있고, 이런 프로그램을 OOP(Object-Oriented Programming), 객체 지향 프로그래밍이라고 한다.

절차 지향 프로그래밍

과거의 프로그래밍은 절차지향적으로 만들었는데, 일련의 순서대로 프로그래밍하는 것을 의미한다. 예를 들어,

기상 -> 씻기 -> 밥 먹기 -> 버스 타기 -> 요금 지불 -> 도착

이런 순이다.

객체 지향 프로그래밍

반면 객체 지향 프로그래밍은 객체를 정의하고 객체 간 협력을 프로그래밍하는 것이다. 위의 절차 지향 과정을 객체로 표현하자면 학생, 밥, 버스 등이 있다. 그리고 ‘밥 먹기’ 같은 행동은 ‘학생’이라는 객체와 ‘밥’이라는 객체가 있어 학생이 밥을 먹는 협력으로 이루어진다. 버스 또한 버스와 학생 간의 상호작용이 일어나는 것이다. 이렇듯 객체 지향 프로그램은 먼저 객체를 만들고 객체 사이에 일어나는 일을 구현하는 것이다.

클래스

객체 지향 프로그램은 클래스를 기반으로 프로그래밍하는 것이다. 클래스는 객체의 속성과 기능을 코드로 구현하는 것이다. 학생 객체를 생각해 보자. 학생은 어떤 정보를 갖고 있을까? 크게 학번, 이름, 학년, 주소 등을 가지고 있을 것이다. 이런 속성들을 특성이라고도 하고 클래스 내부에 변수로 선언한다. 이것들을 ‘멤버 변수’라고 한다. 아무튼 이런 멤버 변수들은 모든 학생들이 갖고 있고, 정보들은 각 객체들마다 다르게 갖고 있을 것이다. 1학년인 철수도 있고, 2학년인 영희도 있을 것이다.

예전부터 클래스를 비유할 때는 클래스는 붕어빵 틀이고, 객체는 붕어빵 틀로 만들어진 붕어빵이라 볼 수 있다. 붕어빵 모양은 같지만 내용물은 하나는 단팥이 될 수도 있고, 다른 하나는 슈크림이 될 수도 있다.

클래스 정의는 다음과 같다.

(접근 제어자) class 클래스이름 {
    멤버 변수;
    메서드;
}

// 실전
public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;
}

클래스 이름 짓는 규칙으로는 단어들의 대문자로 시작한다. 소문자로 시작한다고 오류가 생기는 건 아니지만 관습적으로 사용한다. ex) StudentTest

멤버 변수를 선언할 때 int형, double형 같은 기본 자료형(primitive)으로 선언할 수도 있고, 클래스형으로 선언할 수 있다.

다음은 기능에 해당하는 메서드이다. 멤버 변수들을 활용하여 객체와 관련된 기능을 구현하면 된다. 예를 들어 학생에게 이름을 부여하거나 사는 곳을 출력하는 등이 있다. 메서드 선언은 다음과 같다. 세부 내용은 밑에서 더 다루겠다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    // 메소드 부분
    public void showStudentInfo() {
        System.out.println(studentName + "," + address)
    }
}

패키지

패키지는 클래스 파일의 묶음이다. 패키지를 만들면 프로젝트 하위에 물리적으로 디렉터리가 생성된다. 또한 패키지는 계층 구조를 가질 수 있다. 계층 구조를 구성하는 것은 전체 프로젝트의 소스 코드를 어떻게 관리할지와 관련이 있다. 만약에 학교와 관련된 프로젝트가 있다면 프로젝트 안에 학생, 과목, 교실, 교수, 학과 등의 클래스로 만들 수 있고, 이런 클래스를 협력하는 여러 클래스를 넣을 수 있다.

패키지 선언은 다음과 같다.

package domain.student.view;

public class StudentView {}

클래스 이름은 StudentView인데, 전체 이름(class full name)은 domain.studnet.view.StudentView가 된다. 클래스 이름이 같다고 해도 패키지 이름이 다르면 클래스 전체 이름이 다른 것이므로 다른 클래스가 된다. 즉, 다른 패키지에 있으면 클래스명이 같아도 서로 연관이 없다.

메서드

메서드는 함수의 일종인데, 함수는 ‘하나의 기능을 수행하는 일련의 코드’를 말한다. 우리가 특정 입력을 넣으면 그에 맞게 함수 내에서 연산하고 결과값을 돌려준다. 물론 입력값이 없거나 결과값도 없이 만들 수도 있다. 즉 함수는 어떤 기능을 수행하도록 미리 구현해 두고 필요할 때마다 호출하여 사용하는 것이다. 함수를 정의하는 것은 다음과 같다. (메서드도 똑같다.)

(반환형) 함수이름 (매개변수...) {
    내용
}

// 실전
int add (int num1, int num2) {
    int result;
    result = num1 + num2;
    return result;
}

함수 이름은 add이고, 매개 변수로는 num1, num2이고 int 형으로 반환하겠다고 만든 함수이다. return은 반환 값이라 해서 결과 값을 돌려줄 때 사용하는 것이다. 또는 return만 쓸 수 있는데, 이것은 함수 수행을 끝내고 호출한 곳으로 돌아갈 때도 쓸 수 있다. 메서드의 이름은 첫단어는 소문자로 시작하고 이후 단어의 시작은 대문자로 시작하면 된다. 이를 낙타의 등과 같은 형태라 하여 camel notation이라고 한다. ex) setStudentName

실습해볼 코드는 다음과 같다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String name) {
        studentName = name;
    }
}

Student클래스로 멤버 변수와 메서드로 구성되어 있다. 이제 이것을 활용해서 드디어 객체를 만들어 볼 것이다. 여기서 잠깐 main()함수에 대해서도 살펴 보겠다. main() 함수는 자바 가상 머신(JVM)이 프로그램을 시작하기 위해 호출하는 함수이다. 클래스 내부에서 만들었지만, 클래스의 메서드는 아니다. main() 함수에서 클래스를 사용하는 방법은 두 가지가 있다. 하나는 우리가 만든 클래스 내부에 main() 함수를 만드는 것이고, 또 하나는 외부에 테스트용 클래스를 만들어 사용하는 것이다. 아래서는 클래스 내부에 만드는 방법이다.

public class Student {
    int studentID;
    String studentName;
    int grade;
    String address;

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String name) {
        studentName = name;
    }

    public static void main(String[] args) {
        Student studentHong = new Student();
        studentHong.studentName = "홍석주";

        System.out.println("studentHong.studentName");
        System.out.println(studentHong.getStudentName());
    }
}

다음으론 테스트 클래스를 만들어 사용하는 방법이다. 사용법은 같다.

public class StudetTest {
    public static void main(String[] args) {
        Student studentHong = new Student();
        studentHong.studentName = "홍석주";

        System.out.println("studentHong.studentName");
        System.out.println(studentHong.getStudentName());
    }
}

여기서 Student, StudentTest 클래스는 같은 패키지에 있어서 위처럼 해도 동작하지만 만약, 서로 다른 패키지에 있다면 import문을 사용해서 불러와야 한다.

위에 예제들처럼 클래스를 생성하려면 new 예약어를 사용해야 한다.

클래스형 변수이름 = new 생성자;

이런식의 구조로하여 생성자를 호출하면 새로운 클래스가 생성되고 이것은 메모리 공간(힙 메모리)에 올라간다. 이렇게 사용할 수 있도록 생성된 클래스를 인스턴스라고 하며, 이 인스턴스를 가리키는 클래스형 변수를 참조 변수라고 한다.

힙 메모리는 프로그램에서 사용하는 동적 메모리(dynamic memory) 공간을 말한다. 일반적으로 프로그램은 스택, 힙, 데이터 이렇게 세 영역을 가지고 있는데, 객체가 생성될 때 사용하는 공간이 힙이다. 힙은 동적으로 할당되며 사용이 끝나면 메모리를 해제해 주어야 한다. C나 C++은 프로그래머가 직접 해제해줘야 하는데, 자바에서는 가비지 컬렉터(Garbage Collector)가 자동으로 메모리를 해제해준다.

이 인스턴스를 참조하는 참조 변수를 사용하면 인스턴스의 멤버 변수와 메서드를 참조할 수 있다. 이 때 사용되는 것인 도트(.) 연산자이다. 위 예제 studentHong.studentName = "홍석주";의 부분이 그런 것이다.

인스턴스를 만들게 되면 힙 메모리에 올라가게 되며 이를 참조 변수는 인스턴스를 가리킨다고 했다. 이를 좀 더 명확히 보고자 한다면 참조 변수를 출력해보면 된다.

Student studentHong = new Student();
studentHong.studentName = "홍석주";

Student studentKim = new Student();
studentHong.studentName = "김춘식";

System.out.println(studentHong);
System.out.println(studentKim);

이렇게 하여 결과값을 보면 Student@16f65612, Student@311d617d 등으로 보일텐데 클래스이름@주소값 형태이다. 여기서 나오는 주소값은 해시 코드 값이라고 해서 생성된 객체에 할당하는 가상 주소이다. 이를 확인하여 서로 다른 주소임을 확인하고 다른 객체임을 확인할 수 있다.

생성자

생성자란 클래스를 생성할 때 사용된다. 이전까지는 person.name = “홍석주” 이런식으로 클래스를 만든 후 도트 연산자로 해줬는데, 생성자를 활용하면 우리는 초기값들을 세팅해줄 수 있다.

Person person = new Person();

디폴트 생성자

우리는 생성자라는 것을 만들어 준적이 없는데, 알아서 실행이 되고 있던 것이다. 이것은 자바에서 클래스를 만들면 생성자가 없으면 기본적으로 컴파일할 때 디폴트 생성자를 만들어 주기 때문이다. 컴파일러가 만들어 주는 생성자는 다음과 같다.

public class PErson {
    String name;
    float height;
    float weight;

    // 아무것도 없는 디폴트 생성자이다.
    public Person() {}
}

그렇다면 우리가 임의로 생성자를 만들어 보자.

public class PErson {
    String name;
    float height;
    float weight;

    
    public Person(String name) {
        this.name = name;
    }
}

여기서 this란 객체 자신을 가리키는 것이다. 이 this를 활용하면 각 객체들마다 자신만의 값을 선언하여 활용할 수 있게 되는것이다. 여기서 주의할 점은 위처럼 생성자를 만든다면, 디폴트 생성자는 안 만들어진다. 이점은 유의해서 사용하자. 필요하면 디폴트 생성자도 만들어 주면 된다.

생성자 오버로드

디폴트 생성자라던가 임의의 생성자를 여러개 만들어 두는 경우를 생성자 오버로드라고 한다. 자바에선 이런 기능을 활용하여 매개변수가 다른 생성자를 여러 개 만들 수 있고, 필요에 따라 사용하면 된다.

정보 은닉

우리는 계속해서 public이라는 예약어를 많이 사용했다. public은 접근 제어자 중 하나이다. 만약에 public이었던거를 private로 바꾼다면 다른 클래스에서 이를 사용할 수 없다. 접근 제어자를 활용하면 변수나 메서드가 어디까지 쓰일지를 정할 수 있다. private를 사용하면 다른 클래스에서 못 사용하니 보안적으로는 좋아지나 사용은 필요할 것이다. 그렇다면 어떻게 해야할까. 그렇게 해서 만드는 메서드로 getter, setter 메서드들이다. 예시를 보자

public class MyDate {
    public static void main(String[] args) {
        private int day;
        private int month;
        private int year;

        public setDay(int day) {
            if(month == 2) {
                if(day < 1 || day > 28) {
                    System.out.println("오류입니다.");
                } else {
                    this.day = day;
                }
            }
        }
    }
}

MyDate클래스가 있고, day, month, year에 대해서 private 접근자를 지정했다. 만약에 변수들이 public이었다면, 사용자가 막 지정할 것이다. 그렇지만 private로 되어있기에 setter()함수를 사용해야 하고, 이 함수에서 유효성 같은 것들을 관리하여 값을 올바르게 할당할 수 있게 된다. 아래는 접근제어자 표이다. |접근 제어자|설명| |—–|—-| |public|외부 클래스 어디에서나 접근할 수 있다.| |protected|같은 패키지 내부와 상속 관계의 클래스에서만 접근할 수 있고 그 외 클래스에서는 접근 불가| |아무것도 없는 경우|default이며 같은 패키지 내부에서만 접근할 수 있다.| |private|같은 클래스 내부에서만 접근할 수 있다.|

자바(4) - 제어 흐름

|

제어 흐름

제어 흐름을 통해 우리는 분기를 하여 상황에 맞는 동작을 할 수 있게 해주고 반복되는 일을 효율적으로 처리해줄 수 있다. 대표적으로 if-else와 for, while문이 있다. switch-case도 있는데 글쎄 간단한 결과값일 때는 괜찮을거 같은데 잘 안쓰이는 거 같다. 실무에서는 또 어떻게 될지 모르겠다. (얼른 취업해보고 싶다~~)

조건문

조건문이란 주어진 조건에 따라 다른 문장을 선택할 수 있도록 프로그래밍하는 것으로 if-else, switch-case가 있다.

if-else

문법은 다음과 같고 조건식에는 이전에 배운 논리 연산자를 이용한다. if 조건식이 true이면 수행문1이 동작되고, false 이면 else 가 실행된다.

if(조건식1) {
    수행문1
} else {
    수행문2
}

여러 조건식을 걸어야 할 때도 있기에 else if문을 추가할 수 있다. if와 else는 한 if-else 세트에서 하나씩만 존재가능한데, else if는 여러개를 넣어도 무방하다.

if(조건식1) {
    수행문1
} else if(조건식2) {
    수행문2
} else {
    수행문3
}

switch-case

switch-case는 조건값이 단순할 때 사용하기 좋은데 다음과 같다.

switch(rank) {
    case 1: medalColor = 'G';
            break;
    case 2: medalColor = 'S';
            break;
    default: medalColor = 'A';
}

rank값이 무엇이냐에 따라 분기하기 좋다. 그래서 주로 int형이나 char형일 때 사용하기 좋다. 단, 주의할 점으로 case마다 break를 써주어야 하는데, 안 써주면 이후의 case도 다 걸리기 때문이다. 예를들어 위의 코드서 전부 break없다 치고 case 1이면 medalColor는 G가 되었다가 case2으로 내려가 S가 된다. 그렇기에 꼭 break를 써주어야 한다. default는 아무 case에 속하지 않을 때 기본적으로 주기 위해 있는 것이다.

반복문

반복문은 특정 동작을 반복적으로 수행할 때 사용하는 것이다. 숫자와 함께 5번 내이름을 출력한다 생각하자. 그럼 다음과 같다.

System.out.println("미소여우1");
System.out.println("미소여우2");
System.out.println("미소여우3");
System.out.println("미소여우4");
System.out.println("미소여우5");

같은 내용 5번 출력은 어렵지 않다. 그런데 1000번 출력하라면? 1억번 출력하라면? 많이 무리일 것이다. 이럴 때 우리 단순 노동을 잘해주는 컴퓨터에게 반복적인 일을 맡기면 된다. 이를 위해 for문과 while문을 사용한다. do while도 있는데, 필자는 거의 사용을 안해보았다.

while

while문의 구조는 다음과 같다.

while(조건식) {
    수행문
}

// 실제
int i=0;
while(i<5) {
    System.out.println(i);
    i++;
}

위에서 배운 if처럼 조건식이 참이어야 수행문이 실행된다. i를 출력하고 1씩 증가시키고, i가 6이 되면 false가 되어 종료된다. while문의 대표적인 활용으로는 조건식에 true를 넣어 의도적으로 loop를 걸 수 있다. 이를 통해 특정일을 반복하다가 멈출 필요 있을 때 멈추게 한다.

for

for문의 구조는 다음과 같다.

for(초기화식;조건식;증감식) {
    수행문;
}

// 실제
for(int i=0; i<5; i++) {
    System.out.println(i);
}

초기화 한 i가 조건식이 참일 때까지 반복시킨다.

continue, break

while과 for문 둘 다 continue와 break를 사용해 의도적으로 건너뛰거나 반복문을 중지할 수 있다.

for(int i=0; i<10; i++) {
    if(i%2==0) continue;
    System.out.println(i);
}

짝수인 경우 다시 continue를 만나 이후 내용은 건너뛰게 되고, 홀수만 출력하게 된다.

int i=0;
while(true) {
    if(i == 100) break;
    i++;
}

loop인 상태에서 1씩 증가시키다 i가 100이 되면 break를 만나 반복문이 종료된다.

자바(3) - 연산자

|

연산자란

연산자란 연산에 사용되는 기호이며, 항은 연산에 사용되는 값을 의미한다. 예를 들어 3+4가 있으면, 3과 4는 항이고 +가 더하기 연산자이다. 연산자는 크게 단항, 이항, 삼항 연산자가 있다.

연산자 설명 연산 예
단항 연산자 항이 한 개 ++num
이항 연산자 항이 두 개 num1+num2
삼항 연산자 항이 세 개 (5>3) ? 1:0

증가, 감소 연산자

단항 연산자로 값을 1만큼 늘리거나 줄일 때 사용한다.

연산자 기능 연산 예
++ 항의 값에 1을 더함 val=++num // 먼저 num값이 1 증가 후 val 변수에 대입
val = num++ // val 변수에 기존 num값을 대입 후 1증가
항의 값에 1을 뺌 val = –num
val = num–

관계 연산자

이항 연산자로 두 개의 항 중 어느 것이 큰지, 작은지, 같은지 여부 판단 시 사용한다. 결과 값으로는 boolean값인 true와 false를 반환한다. |연산자|기능| |—–|—-| |>|왼쪽이 오른쪽보다 크면 참, 아니면 거짓| |<|왼쪽이 오른쪽보다 작으면 참, 아니면 거짓| |>=|왼쪽이 오른쪽보다 크거나 같으면 참, 아니면 거짓| |<=|왼쪽이 오른쪽보다 작거나 같으면 참, 아니면 거짓| |==|두 개 항이 같으면 참, 아니면 거짓| |!=|두 개 항이 다르면 참, 아니면 거짓|

논리 연산자

두 항을 비교하여 참 거짓을 판단한다. 관계 연사자와 함께 사용한다. |연산자|기능| |—–|—-| |&& (논리 곱)|두 항이 모두 참인 경우에만 참. 그 외에는 모두 거짓이다.| ||| (논리 합)|두 항 중 하나의 항만 참이면 참. 두 항이 모두 거짓일 때 거짓이다.| |! (부정)|단항 연산자로 참인 경우 거짓으로, 거짓인 경우 참으로 바꿔준다.|

단락 회로 평가

논리 연산자를 사용할 때 논리 곱(&&) 연산은 두 항이 모두 참일 때만 결과 값이 참이 된다. 즉 하나라도 거짓이면 결과가 거짓이 된다. 논리 합(||)은 하나만 참이면 나머지와 상관없이 참인데 이런 특성 때문에 특이한 일이 벌어진다.

int num1 = 10;
int i = 2;

boolean value = ((num1 = num1 + 10) < 10) && ((i = i + 2) < 10);
System.out.println(value);
System.out.println(num1);
System.out.println(i);

value = ((num1 = num1 + 10) > 10) || ((i = i + 2) < 10);
System.out.println(value);
System.out.println(num1);
System.out.println(i);

위의 코드의 실행 결과를 보면 다음과 같다.

false 20 2 true 30 2

이렇게 나오는 이유는 첫 번째 value의 (num1 = num1 + 10) < 10 부분때문이다. num1에 10을 더해 20이 되고 이것이 10보다 작냐인데, 거짓이 나온다. 이어서 나오는 &&연산에 의하면 첫 번째가 거짓이니 두 번째는 볼 것도 없이 판단하지 않는다. 그렇기에 i는 계산이 되지 않는다.

두 번째 value는 ||연산이 나오는데, 이번에는 (num1 = num1 + 10) > 10) 이 부분이 참이기에 뒤에거를 판단하지 않아 i가 계산되지 않는다. 이처럼 논리 곱이나 합 연산 시 두 항을 모두 실행하지 않아도 결과 값을 알 수 있으면, 나머지 항은 실행되지 않는데 이것을 단락 회로 평가(Short Circuit Evaluation:SCE)라고 한다.

복합 대입 연산자

대입 연산자와 다른 연산자를 조합해 하나의 연산자처럼 사용하는 것을 말한다. |연산자|기능|연산 예| |—–|—-|—–| |+=|두 항의 값을 더해 왼쪽 항에 대입|num1 += 2;| |-=|두 항의 값을 빼서 왼쪽 항에 대입|num1 -= 2;| |*=|두 항의 값을 곱해서 왼쪽 항에 대입|num1 *= 2;| |/=|왼쪽 항을 오른쪽 항으로 나누어 몫을 왼쪽 항에 대입|num1 /= 2;| |%=|왼쪽 항을 오른쪽 항으로 나누어 나머지를 왼쪽 항에 대입|num1 %= 2;|

조건 연산자

삼항 연산자이다. 주어진 조건식이 참인 경우와 거짓인 경우 다른 결과 값이 나온다. |연산자|기능|연산 예| |—–|—–|——| |조건식?결과1:결과2|조건식이 참이면 결과1, 조건식이 거짓이면 결과2가 선택됨|int num = (5>3)?10:20;|

비트 연산자

비트 연산자는 비트 단위로 연산이 이루어지는 연산자이다. 비트 연산을 이용하면 어떤 수의 2배수, 4배수 만들어 속도를 빠르게 할 수도 있다.

비트 논리 연산자

비트 단위로 &, |, ^, ~ 연산이 이루어진다.

&(AND) 연산자

두 개의 비트 값이 모두 1인 경우에만 1이 된다.

int num1 = 5;
int num2 = 10;
int result = num1 & num2;

을 예시로 들어 보자. 각 num들을 비트로 풀어보면 num1: 00000101 num2: 00001010 각 자리마다 비교하여 둘 다 1일 시만 1이 된다. 결과: 00000000

|(OR) 연산자

하나라도 1이면 1이 된다. 위 예시대로 해보며 하나라도 1이면 1이 된다. 결과: 00001111

^(XOR) 연산자

비트가 같은 값이면 0을 다른 값이면 1이 된다. X가 exclusive라는 배타적인 의미인데 서로 배타된단 의민듯 하다. 결과: 00001111

~(반전) 연산자

1은 0으로 0은 1로 반전 해주면 된다.

int num = 10;
int result = ~num;

num은 00001010이 되고 result는 11110101이 된다. 부호비트가 1로 바뀌어서 음수가 되었는데, 음수는 양수로 변환해야 얼마인지 알기에 2의 보수를 취해본다. 00001010(1의 보수) -> 00001011(2의 보수) 즉 -11이 된다.

비트 이동 연산자

«, », »> 세가지가 사용 된다. 이를 시프트(shift)연산자라고도 한다.

« 연산자

왼쪽으로 비트를 이동한다는 의미이다.

int num = 5;
num << 2;

num은 00000101인데 이를 2비트씩 왼쪽으로 미는 것이다. 그러면 00010100이 되고 20이 된다. n비트 왼쪽 이동한다는 것은 기존 값에 n^2만큼 곱해지는 뜻이다.

» 연산자

오른쪽으로 비트를 이동하는 연산자이다. 위의 예시서 «를 »로 바꿔보자. 00001010이 있으면 오른쪽으로 2비트씩 이동한다. 그러면 0000010이 된다. 오른쪽으로 n비트 이동하면 n^2만큼 나뉘는 것이다.

»> 연산자

>>와 마찬가지로 비트를 오른쪽으로 이동하는 것인데, 왼쪽에 채워지는 비트값은 부호 비트와 상관없이 무조건 0이 된다.