728x90

삽질기

모든 코드는 깃허브에 있다.

일단 저번에 마이그레이션 1탄을 했었는데 의존성이 겹쳐서 다시 리팩토링 하려고 보니까 에러가 많았다.

의존성 겹침 에러

일단 의존성이 겹쳐서 생긴 에러가 조금 있다.

부분만 적은 gradle 일부이다.

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

dependencies {
    implementation (
            'org.springframework.boot:spring-boot-starter-web', //이부분
            'org.springframework.boot:spring-boot-starter-test', //이부분
            'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0',
            'org.mariadb.jdbc:mariadb-java-client', //이부분
            'commons-io:commons-io:2.6',
            'commons-fileupload:commons-fileupload:1.3.3',
            'javax.servlet:jstl',
            'org.projectlombok:lombok',
    )
}

이렇게 주석처리한 부분들이 에러를 띄웠다.

각자의 버전마다 호응(?)이 되는 버전들이 맞아야 세팅이 완료가 되는데 서로 충돌해서 발생하는 에러였다.

그레이들에서

id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'

이 두가지의 부분이 중요한데,

org.springframework.boot는 부트 버전을 명시하여 boot starter 관련 된것들을 통일 시켜주고

dependency-management는 다른 라이브러리들의 의존성을 관리해주는 것인데 최적화된 버전들을 맞춰주는 역할을 한다.

그러면서 버전이 맞지않는게 생기면 에러를 띄워준다.

톰캣 오류

그리고 tomcat 오류였다.

부트와 jsp는 잘 맞지않는다.

그럼에도 일단 jsp가 너무 많아서 일단 jsp를 사용하려고 했기 때문에

의존성에는

implementation (
    'javax.servlet:jstl',
    'org.apache.tomcat.embed:tomcat-embed-jasper'
)

두가지를 일단 추가 해줬다. JSP를 사용하려면 embed-jasper가 존재해야 하는데

이건 기본으로 작성해줘서 문제가 아니었다. 😰

application.properties

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

여기는 둘다 잘 해주었는데.. 왜문제일까 약 2시간 고민했다 😡

근데 여기가 문제였다...

스크린샷 2021-08-21 오후 5 45 32

Working Directory에 저 설정을 안해주면 jsp 파일을 안찾는다. 그냥 반응이 없음..

@Controller @RequestMapping 이런건 문제가 아니었다.

Bean 주입

Bean을 예를 들어

public interface TestInter {
    void hello();
}

@Repository
public class TestImpl implements TestInter {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

@RequiredArgsConstructor
public class TestService {
    private final TestInter testInter;
}

스프링5 프로그래밍 입문

여기서 봤던건데 스터디원 한분이 알려줬다. 그러면서 책에있는게 기억이 나더라...

결론은 빈을 뭘 주입해야할지 모르는것이다.
@Qualifier를 통해 명시해줘서 하거나 이전 회사에서 하던 방식으로 어노테이션 뒤에 (빈 이름) 해서 매칭해주는 식으로 해야한다. 이게 좋지 않아서 다 삭제했다.

스크린샷 2021-08-21 오후 5 54 26

이랬던 구조들을

스크린샷 2021-08-21 오후 5 54 42

이렇게 바꾸었다. 클래스 이름들은 추후 안정화가 되면 바꿀 계획이다.

학원에서부터 벗어난지 꽤됐고, 그러면서 TDD강의까지 듣고있고 그동안 공부했던거를 생각하면서 코드를 보니까
더러운 부분이 참많다.

이제 시작이고 끝을 보겠다. 계속 리팩토링 하는 과정을 블로그 + 깃허브 에 업데이트 할 것이다.

728x90

'Spring' 카테고리의 다른 글

Filter, Interceptor 정리  (0) 2022.08.07
Jasypt  (0) 2022.08.07
Spring Data JPA  (0) 2022.08.06
Spring -> Spring Boot 마이그레이션  (0) 2022.08.05
728x90

Spring Data JPA

JPA를 사용해도 마찬가지로 DB의 기본적인 CRUD는 어찌됐든 사용하기 마련이다.
근데 이것을 가지고 똑같이 계속 사용해서 엔티티마다 만들어 주는 개념이 아니라
인터페이스에 제네릭 타입을 사용해서 엔티티를 매칭해주어 구현을 하는것이 편리하다.

이 repository를 개발할 때 인터페이스만 정의주면 실행 시점에 Spring Data JPA가 구현 객체를 동적으로 생성해서 주입해준다.
그래서 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발이 가능 하다.

public class MemeberRepository extends JpaRepository<Member, Long> {
    List<Member> findAll();
}

이런식으로 구현을 해준다.
MemberRepository에는 @Repository 를 넣어주지 않는다.

왜냐면

JpaRepository를 상속받은 JpaRepositoryImplementation 를 구현한
SimpleJpaRepository

image
{: text-center}

그림과 같이 @Repository 를 가지고 있다.

그래서 인터페이스만 잘 작성해주면 된다.

제네릭에는 JpaRepository<엔티티, 식별자타입> 이렇게 만들어준다.

JpaRepository의 주요 메서드

  • save() : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
  • delete() : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 를 호출한다.
  • findOne() : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 를 호출한다.
  • getOne() : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 를 호출한다.
  • findAll() : 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 전달할 수 있다.
  • findBy() : 엔티티 하나를 조회한다. Optional 타입의 객체를 반환한다.

반환 타입

Spring Data JPA는 만약 조회 결과가 없는 경우 컬렉션을 반환하는 메소드에서는 null을 반환한다.

단건으로 두 건 이상 겹쳐서 조회가 될 때 다음의 예제를 보자.

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByName(String name);
}

@DataJpaTest
class MemberTest {
  @Test
  void 두건_조회_테스트() {
      Address address = Address.builder()
          .city("city")
          .street("street")
          .zipcode("zipcode")
          .build();

      Address comAddress = Address.builder()
          .city("cocity")
          .street("costreet")
          .zipcode("cozipcode")
          .build();

      Period period = Period.of("20210714", "20210714");

      Member member = Member.builder()
          .name("name")
          .age(26)
          .period(period)
          .homeAddress(address)
          .companyAddress(comAddress)
          .build();

      Member member2 = Member.builder()
          .name("name")
          .age(26)
          .period(period)
          .homeAddress(address)
          .companyAddress(comAddress)
          .build();

      memberRepository.save(member);
      memberRepository.save(member2);

      memberRepository.findByName("name");
  }
}

같은 삽입 코드를 두개 복사했다. 감안해서 보자.

일단 Member 엔티티인 member, member2를 저장하고 Name 컬럼의 값으로 하나를 가져오는 것인데, 복사해서 붙여넣었으니 같은 값이 즉 2개 이다.
하나의 데이터만 가져오지 못한다면,

image
{: text-center}

NonUniqueResultException 을 발생시킨다.

정확하게 1개 반환하는 것이 아니라면 List로 받는게 오히려 좋을 수도 있겠다.

findById()와 findAll()의 차이를 알고 싶다면

쿼리 메서드

Spring Data JPA가 제공하는 기능이다. 메소드 이름만으로 쿼리를 생성하는 기능이 있는데 인터페이스에 메소드만 선언해주면 해당 메소드 이름으로의 적절한 JPQL 쿼리를 생성해서 날려준다.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 사용하여 인터페이스에 직접 정의

쿼리 메소드의 규칙에 따라서 정의를 해주어야 동작한다.

쿼리 메소드 규칙

키워드 JQPL 예
And findByNameAndAddress where x.Name = ?1 and x.Address = ?2
Or findByNameOrAddress where x.Name = ?1 or x.Address = ?2
Distinct findDistinctByLastnameAndFirstname select distinct … where x.lastname = ?1 and x.firstname = ?2
Is、Equals findByFirstname, findByFirstnameIs, findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull、Null findByAge(Is)Null … where x.age is null
IsNotNull、NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 ( %가 뒤에 추가된 매개 변수)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 ( %가 앞에 추가된 매개 변수)
Containing findByFirstnameContaining … where x.firstname like ?1 ( %가 래핑된 매개 변수)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

NamedQuery

보통 @NamedQuery 로 정의하는 방식인데, 이거는 MyBatis를 쓰는 방식같은 느낌이 많이 든다.

이것은 전의 포스팅을 보면 이해가 쉬울 수 있다.

@Query, Repository 메소드에 쿼리 정의

이 방법이 더 간단하고 편리하게 정의할 수 있는 느낌이 더 강하다.

이름없는 NamedQuery 라고 생각하면 좋을듯? 싶다 😁

장점이라고 하면 애플리케이션 실행 시점에 문법 오류를 발견할 수 있어서 좋다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Memer m where m.name = ?1")
    Member findByName(String name);
}

사용은 이런식으로 하면 되겠다.

Native SQL을 사용하려면 @Query("sql", nativeQuery=true) 로 설정하면 되고 이 네이티브 쿼리에서는 파라미터 바인딩 숫자가 0부터 시작한다.

0부터 시작하면 뭐하나 인덱스 밀려서 쓰면 값 에러 날 수 있으니 권장하지 않는다.

위치 기준 파리미터 주의점

위치(순서) 기준보다는 이름 기준의 바인딩을 추천한다. 위치 기준의 바인딩을 사용하면 추후에 요소가 추가되면 밀리거나 땡겨질 수가 있기 때문❗️

그래서 아래와 같은 방법을 더 많이 쓴다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Memer m where m.name = :name")
    Member findByName(@Param("name") String name);
}

정렬

Spring Data JPA에는 쿼리 메소드에 페이징 그리고 정렬기능을 사용하도록 2가지 파라미터를 제공한다.

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort) 포함

여기서 정렬 실습에 대한 에러도 같이 남긴다.

우선 sql DDL 그리고 DML 이다.

member.sql

CREATE TABLE IF NOT EXISTS MEMBER
(   ID              BIGINT GENERATED BY DEFAULT AS IDENTITY,
    NAME            VARCHAR(255),
    AGE             INTEGER NOT NULL,
    COMPANY_CITY    VARCHAR(255),
    COMPANY_STREET  VARCHAR(255),
    COMPANY_ZIPCODE VARCHAR(255),
    CITY            VARCHAR(255),
    STREET          VARCHAR(255),
    ZIPCODE         VARCHAR(255),
    END_DATE        VARCHAR(255),
    START_DATE      VARCHAR(255),
    PRIMARY KEY (ID)
);

INSERT INTO MEMBER (name, age, company_city, company_street, company_zipcode, city, street, zipcode, end_date, start_date) VALUES ('테스트', 33, 'city1', 'street', 'zipcode', 'cocity1', 'costreet', 'cozipcode', '20210714', '20210714');
같은 쿼리 여러개 넣었다고 생각하자.
...
...
...

처음에 이렇게 쿼리메소드를 구현하였다.🙂

public interface MemberRepository extends JpaRepository {
    //기존 로직
    ...

    Page<Member> findByName(String name, Pageable pageable);
}

그리고 테스트코드에서는...

Page<Member> members = memberRepository.findByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));

이렇게 Page<T>를 사용하면 페이징 처리를 할 수있다.

첫 페이지는 0부터 시작한다.

이슈 발생

여기서 생긴 이슈는 분명 Sort.by(Order.desc("정렬할 컬럼")); 으로 정렬을 넣어줬음에도 쿼리에서는 정렬을 해주지 않았고 근데 정렬방식은 DESC로 나오는 경우였다.

findByName
{: text-center}

result
{: text-center}

이렇게 페이징을 위해 count쿼리는 나온다. 근데 where 이후로 order by 가 없는 것이다.

스터디를 하는 분과 같이 의논을 Code With Me로 같이 삽질을 좀 했다.

p.s 시간 내주셔서 감사합니다!

결과 알아낸 것은 내가 쿼리 메소드로 구현한건 findBy 로 시작하는 쿼리메소드를 오버로딩해서 Pageable만 추가해줬다.

이렇게 하니까 결국 기본으로 Spring Data JPA에서 정의된 쿼리메소드 참조를 하기 때문에 우선순위가 그쪽으로 배정되어서 조회는 ASC로 되는가보다.

해결책

public interface MemberRepository extend JpaRepository {
    Page<Member> findMembersByName (String name, Pageable pageable);
}

보다시피 메서드 이름을 findMembersByName 으로 바꿨고,

@DataJpaTest
@ActiveProfiles("test")
public class DbTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Sql("classpath:member.sql")
    void dbTest() throws Exception {
        Page<Member> members = memberRepository.findMembersByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));


        Pageable pageable = members.getPageable();

        System.out.println("pageable = " + pageable);
        System.out.println("pageable.getSort() = " + pageable.getSort());
    }
}

이렇게 해주니까

findMembersByName
{: text-center}

잘 나오게 된다.

2021-08-22 수정본

위 부분은 사실 단순한 해결이었고, 진정한 해결책은 우선순위는 @NamedQuery 에 있다.

실습때문에 Member 엔티티에 NamedQuery를 추가했었어서 이 부분이 먼저 구현되어 비정상적으로 정렬이 됐던 것이다. 스터디원들 다 같이 모여 해결했다. 이럴때 집단지성이 굉장히 좋은것 같다👍

아무튼 구문자체에는 오류가 없었고 네임드 쿼리 쓰지말자! Mybatis를 보면 볼 수록 생각나게 한다. 이제 정을 떼는것이 좋을것 같다.

좀 더 편리하게 사용하려면 interface다중상속이 가능하기 때문에
이렇게 상속해서 사용해도 될 것 같다.

이상으로 12장 포스팅을 마치도록 하겠다.

728x90

'Spring' 카테고리의 다른 글

Jasypt  (0) 2022.08.07
Spring -> Spring Boot 마이그레이션 2  (0) 2022.08.06
Spring -> Spring Boot 마이그레이션  (0) 2022.08.05
[Spring] MockMvc Bean 주입 에러  (0) 2022.08.04
728x90

연산자, 조건문 정리

  • 산술 연산자
  • 비트 연산자
  • 관계 연산자
  • 논리 연산자
  • instanceof
  • assignment(=) operator
  • 화살표(->) 연산자
  • 3항 연산자
  • 연산자 우선 순위
  • (optional) Java 13. switch 연산자

산술 연산자(Arithmetic Operator)

산술 연산자는 사칙연산을 다루는 연산자로, 가장 기본적이면서 가장 많이 사용되는 연산자 중의 하나.

산술 연산자는 모두 두 개의 피연산자를 가지는 이항 연산자이며, 결합 방향은 왼쪽에서 오른쪽이다.

연산자 설명
+ 더하기 연산 수행(문자열 연결 가능)
- 마이너스 연산 수행
* 곱하기 연산 수행
/ 나누기 연산 수행(정수형은 몫 연산자)
% 나머지 연산 수행
int result = 1 + 2;
System.out.println(result);

String ss = "연습";
System.out.println(ss + " 입니다.");

int product = 5 * 4;
System.out.println(product);

System.out.println("정수형 나누기");
System.out.println(5 / 4);
System.out.println(5 % 4);

System.out.println("실수형 나누기");
System.out.println(5.0 / 4.0);
System.out.println(5.0 % 4.0);


System.out.println(5 / 0); // java.lang.ArithmeticException: / by zero

System.out.println(5.0 / 0.0); // Infinity

System.out.println(5.0 % 0.0); // NaN

System.out.println(-5.0 / 0.0); // -Infinity

System.out.println(-5.0 % 0.0); // NaN

정수형일때 조심해야 하는 점은 보는 것과 같이 나눗셈 연산을 수행할 때이다.
그냥 실행이 잘 되기 때문에 % 연산을 수행할 때에도 음수값으로 나눌때 값이 다르다.

실수형일때는 연산 과정에서는 오차가 발생할 수 있고 이것을 조심해야 한다.
0으로 나누거나 나머지 연산을 사용하면 Infinity, NaN 가 나온다.

단항 연산자

  • + 를 붙이면 양수를 나타냄. (생략 가능)
  • - 를 붙이면 음수 값을 나타낸다.

증감 연산자

++를 붙이면 값을 1씩 증가시킨다. 하지만 피연산자의 위치에 따라 계산이 살짝 달라진다.

앞쪽에 ++변수 의경우 식을 진행하기 전에 1을 증가시키고, 뒤쪽변수++에 위치하는 경우 식을 진행한 후에 1을 증가시킨다.

-- 도 이와 같다.

int result = 1;
for (int i = 0; i < 5; i++) {
    System.out.print(result++ + " ");
}

System.out.println();
int result2 = 1;

for (int i = 0; i < 5; i++) {
    System.out.print(++result2 + " ");
}

image

두 증감 연산자에 대한 차이이다.

비트 연산자 (Bitwise Operator)

논리 연산자와 비슷하지만, 비트 단위로 논리 연산을 할 때 사용하는 연산자이다.

비트 단위로 왼쪽이나 오른쪽으로 전체 비트를 이동하거나 1의 보수를 만들 때도 사용된다.

연산자 설명
& 대응되는 비트가 모두 1이면 1을 반환함.(AND 연산)
대응되는 비트중 하나라도 1이면 1을 반환함.(OR 연산)
^ 대응되는 비트가 서로 다르면 1을 반환함.(XOR 연산)
~ 비트를 1이면 0으로, 0이면 1로 바꿈(NOT 연산, 1의 보수)
<< 명시된 수 만큼 비트를 전부 왼쪽으로 이동(left shift 연산)
>> 부호를 유지하면서 지정한 수 만큼 전부 오른쪽으로 이동(right shift 연산)
>>> 지정한 수만큼 비트를 전부 오른쪽으로 이동시키며, 새로운 비트는 전부 0이 됨.

관계 연산자

관계 연산자는 피연산자가 값이 어떤지 비교하는 연산자들이다.

  • == : 같다.
  • != : 다르다.
  • > : 크다
  • >= : 크거나 같다.
  • < : 작다.
  • <= : 작거나 같다.

문자열이 같은지를 보려면 equals()를 사용해야 한다.

==은 주소값을 비교하는 것이기 때문에 String 은 equals를 사용하자.

논리 연산자

피연산자 값으로 boolean을 받고 true/false를 판단할 경우에 사용한다.

  • &&
    • AND 연산자이며, 좌우 값이 둘다 참일 경우에 true, 그렇지 않으면 false를 반환
  • ||
    • OR 연산자로, 한쪽만 참이어도 참을 반환한다.
  • !
    • NOT 연산자로 위의 두개와는 다르게 단항 연산자이며 조건이 거짓일때 true값을 반환해준다. 그러니까 기본이 거짓이면 true, 참이면 false를 반환한다.

instanceOf

객체 타입을 확인하는데 주로 사용하고, 나같은 경우에는 테스트 코드를 작성했을 때, 많이 사용한 메서드이다. 속성 자체는 연산자이고, 형 변환이 가능한지 여부를 true/false 로 가르쳐준다. 부모, 자식 객체인지 확인하는데 쓴다고 생각하면 된다.

class A {

}

class B extends A {

}

public class InstanceTest {
  public static void main(String[] args) {
    A a = new A();
    B b = new B();

    System.out.println(a instanceof A);
    System.out.println(b instanceof A);
    System.out.println(a instanceof B);
    System.out.println(b instanceof B);
  }
}

결과는 true, true, false, true 가 나온다.

b는 부모 클래스인 a를 상속받았기 때문에 A의 객체타입이 맞다.

그래서 true이고 A클래스는 반대로 B에 대한 부모 클래스이기 때문에 B의 객체 타입일 수는 없으므로 false를 출력하게 된다.

assignment(=) operator

할당 연산자는 연산자 기준 오른쪽값을 왼쪽의 피연산자에게 할당한다.

int x = 0; 에서 x라는 int형 변수에 0을 할당하는 것이다.

A a = new A(); 처럼 객체를 할당할 수도 있다.

복합 할당

public class Test {
  public static void main(String[] args) {
      int a = 0;
      a = a + 1;
      a += 1;
  }
}

a = a + 1 의 경우

image1

a += 1 의 경우

image2

서로 바이트 코드가 다르다!!!

아래의 경우가 속도가 조금 빠르지 않을까 생각한다.

화살표(->) 연산자

화살표 연산자는 Java8에서 람다 표현식으로 익명클래스를 대체한 것이다.

public interface Test {
    void test();
}

public class Main {
    public static void main(String[] args){
        Test test = new Test() {
            @Override
            public void test() {
                //로직 구현
            }
        };
    }
}

이런식으로 익명 클래스를 사용하여 구현을 해줬었다.

근데 이 화살표 연산자가 생긴뒤로는 이렇게 구현하지 않고 아래와 같이 구현한다.

@FunctionalInterface
public interface Test {
    void test();
}

Test test = () -> 로직;

이렇게 구현을 해준다.

@FunctionalInterface 를 인터페이스에 붙여주게 되면 interface에는 하나의 추상메서드만 정의가 가능하다.

람다를 다시 공부할때 포스팅 하도록 하겠다.

3항 연산자

삼항 연산자는 조건 ? 참일 경우 : 거짓일 경우 로 구현하는데 피연산자를 세개를 받으므로 삼항 연산자라고 불린다.

if-else문과 비슷한 역할을 수행하지만 한줄로 작성이 가능하다.

public class Ternary {
    public static void main(String[] args) {
        int n1 = 5;
        int n2 = 10;

        int max = (n1 > n2) ? n1 : n2;
        System.out.println("max : " + max);
    }
}

기본적으로 연산자에는 우선순위가 있으며, 괄호의 우선순위가 제일 높고,

산술 > 비교 > 논리 > 대입의 순서이며, 단항 > 이항 > 삼항의 순서다.

연산자의 연산 진행방향은 왼쪽에서 오른쪽으로 수행되며,

단항 연산자와 대입 연산자의 경우에는 오른쪽에서 왼쪽으로 수행된다.

image3

switch 연산자

어떠한 값이 맞는다면 해당하는 식을 수행하게끔 만든 연산자이다.

단일 값으로 평가되는 하나의 표현식.

java 15 버전에서는 case를 case -> 로 표현한다고 한다.

public class Main {
    public static void main(String[] args) {
        String day = "월";

        switch (day) {
            case "월":
                System.out.println("월요일");
                break;
            case "화":
                System.out.println("화요일");
                break;
            case "수":
                System.out.println("수요일");
                break;
            case "목":
                System.out.println("목요일");
                break;
            case "금":
                System.out.println("금요일");
                break;
        }
    }
}

이것을

switch (day) {
    case "월" -> System.out.println("월요일");
    case "화" -> System.out.println("화요일");
    case "수" -> System.out.println("수요일");
    case "목" -> System.out.println("목요일");
    case "금" -> System.out.println("금요일");
    case "토", "일" -> System.out.println("주말");
}

이렇게 표현이 가능하다.

if-else문은 원하는 조건이 나올때까지 순서대로 모든 경우를 비교하고

switch문은 jump-table을 사용해서 한번에 원하는 곳에 이동한다.

그래서 if문은 조건문의 개수만큼 O(n)의 시간복잡도를 갖게 되어 성능에 단점이 있고,

switch문은 case의 개수만큼 jump-table을 차지하므로 메모리에 단점이 있다.

때문에 성능면으로 보면 switch문이 더 빨라서

조건이 3개 이상일 경우에는 switch를 사용하는 것이 더 좋다고 한다.

그렇지만 사실 그 차이는 컴파일러의 처리 속도에 따라 차이가 생기는 것이고

요즘 컴파일러들이 워낙 우수하기 때문에 차이가 미비하다고 하다.

if-else 를 쓰든 switch 를 쓰든 (특별히 성능과 메모리의 이슈를 갖고 있지 않은 이상) 각자의 취향이 아닐까.

가독성을 높이는 쪽으로 선택해서 사용하면 될 것 같고

나는 되도록이면 Early Return을 하려고 한다.

String string;

if (조건) {
    string = "참";
} else {
    string = "거짓";
}

이런게 있다고 하면 조건에 부합하는 문자열만 보내주면 된다.

그래서

String string = getString();

public String getString() {
    if (조건) {
        return "참";
    }

    return "거짓";
}

이렇게 분리하는 방법이 좋은것 같다.

이걸 더 분리한다고 치면 이제 공부했던 State패턴을 사용하면 되겠다.

이거는 다시 정리하는것이기 때문에 나중에 객체지향 개념을 들어가게 되면 구체적으로 정리를 해보도록 하겠다.

728x90

'Java' 카테고리의 다른 글

Oauth 이슈  (0) 2022.08.07
SOLID 원칙  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
JVM  (0) 2022.08.06
728x90

2주차

주차마다 원래 관리를 해주어야 하는 스터디 이지만 이미 끝났으므로 공부하면 바로 포스팅을 할 계획이다.

  • 프리미티브 타입 종류와 값의 범위 그리고 기본 값
  • 프리미티브 타입과 레퍼런스 타입
  • 리터럴
  • 변수 선언 및 초기화하는 방법
  • 변수의 스코프와 라이프타임
  • 타입 변환, 캐스팅 그리고 타입 프로모션
  • 1차 및 2차 배열 선언하기
  • 타입 추론, var

primitive 타입 종류와 값의 범위 그리고 기본 값

자바의 변수 타입에는 primitive와 reference 타입 두가지가 있다.
프리미티브 타입의 경우 정수, 실수, 그리고 논리값 으로 이루어져있다.

이 변수의 타입들의 범위는 조금만 생각해보면 왜 저렇게 정해져있는지 알 수 있다.
연관성은 바로 이진수의 제곱 의 범위 이다.

1bit 당 가질수 있는 경우의 수는 2개이다. 0, 1 이렇게 말이다.
그러니까 비트가 늘어날 때마다 자연스럽게 제곱범위로 늘어나는 것이다.
근데 이것을 반으로 갈라서 음수부분, 양수 이렇게 나누기 때문에
범위는 아래 표와 같다. 근데 반이면 반이지 절대값으로 봤을때 1이 차이가 나는데
그 이유는 0 때문에 그렇다.

타입 설명 사이즈 값의 범위 기본값
byte 부호 있는 정숫값 8bit -128 ~ 127 0
short 부호 있는 정숫값 16bit -32768 ~ 32767 0
int 부호 있는 정숫값 32bit -2147483648~2147483647 0
long 부호 있는 정숫값 64bit -2의63승 ~ +2의63승-1 0L
char 유니코드 문자 16bit '\u0000' ~ '\uffff'/ 0~65535 '\u0000'
float IEEE754 부동소수점수 32bit (3.4 X 10-38) ~ (3.4 X 1038)의 근사값 0.0d
double IEEE754 부동소수점수 64bit (1.7 X 10-308) ~ (1.7 X 10308)의 근사값 0.0d
boolean 논리값 1bit true, false false

프리미티브 타입과 레퍼런스 타입

프리미티브 타입은 stack에 적재가 된다.

반대로 레퍼런스 타입은 Heap내에 저장이 된다.

그래서 참조 타입 변수의 주소값에는 직접적인 값이 아니라 Heap 영역에 할당된 주소가 저장이 된다.

참조형 타입에는 클래스(Class) 안에 문자열(String) 포함, 배열(Array), 열거형(Enumeration), interface 가 있다.

int a = 10;
double b = 19.2;
Car c = new Car("제네시스", "gv80");

image
{: .text-center}

이렇게 저장이 된다고 보면 된다.

참조형 타입에는 빈 객체를 의미하는 null이 존재한다.
예를 들어 객체나 배열을 Null 값으로 받으면 NullPointException이 발생하므로 변수값을 넣어야 한다.

Literal

리터럴은 변수나 상수에 저장되는 값 그 자체이다.

위에서 보면 10이나 19.2가 그에 해당한다.

고정된 값을 표현하는 리터럴은 정수, 실수, 문자, boolean, 문자열 을 표현하는 리터럴 등이 있다.

정수형 리터럴

정수를 표현하는 리터럴은 이렇게 구성되어 있다.

int a = 10;       //10진수 리터럴
int a = 012;      //8진수 리터럴
int a = 0x1a;     //16진수 리터럴
int a = 0b110101; //2진수 리터럴

실수 리터럴

실수 타입의 리터럴은 기본적으로 double 타입이고, float 타입으로 표현하려면 f를 명시적으로 붙여야한다.

double b = 1.1;
double b = 1E-1;
float b = 1.1f; 

문자 리터럴

문자 리터럴은 작은 따옴표 안에 할당할 수 있다.

char a = 'A';

이스케이프 문자열

그리고 이스케이프 문자열을 사용할 수 있다.

이스케이프 문자
{: .text-center}

참조타입인데도 불구하고 리터럴을 지원하는 문자열이다.

문자열 타입은 리터럴 을 지원하는데, 리터럴 방식으로 String 에 값을 주면 Heap 영역에서 String constant pool 이라는 특수한 영역에 값이 저장된다.

동일한 값을 쓰는 경우에 다른 일반적인 레퍼런스타입 처럼 Heap 에 또 올라가지 않고, String constant pool 에 존재하는 값을 참조하는 방식으로 작동한다.

String s1 = "java";
String s2 = "java";

이렇게 하면 둘의 주소는 같다.

스크린샷 2021-08-14 오전 11 37 52

스크린샷 2021-08-14 오전 11 38 11

그래서 위와같이 주소값은 같아지게 된다.

변수 선언 및 초기화 방법

저번 포스팅에서 봤듯이 클래스의 필드에선 자동으로 선언만 해줘도 초기화가 기본값으로 진행이 된다.

선언을 하는 순간 이 변수를 저장할거야 저장공간을 할당해줘 라고 해석된다.

메소드 내에서는 선언만하고 초기화를 안해준다면

java: variable a might not have been initialized

에러가 발생한다.

그래서 선언만 해주고 진행을 해 나가다가 무조건적으로 결국엔 값을 할당해서 초기화를 시켜주어야 한다는 것이다.

선언과 초기화

선언과 초기화를 같이 진행할 수도 있다.

String s;
s = "java";

String s = "java";

두개 다 사용이 가능한 방법

변수의 스코프와 라이프타입

scope

변수의 스코프는 해당 변수를 사용할 수 있는 영역 범위를 뜻한다.
변수의 경우 scope 에 따라 Instance 변수, class 변수, Local 변수 로 나눌 수 있다.

lifetime

해당 변수가 메모리에 언제까지 살아있는지를 의미한다.

인스턴스 변수

인스턴스 변수는 클래스안에서 생성된 변수이며 필드라고 부른다.

  • scope: static 메서드를 제외한 전체
  • lifetime: 해당 클래스를 인스턴스화 한 객체가 메모리에서 제거될 때까지

클래스 변수

class 변수는 클래스 내에 필드에 생성이 되지만 static 키워드가 붙어있는 변수

  • scope: 클래스 전체
  • lifetime: 프로그램 종료시

로컬 변수

Local 변수는 지역변수라고도 부르며 위의 두 변수를 제외한 모든 변수가 이것에 해당된다.

  • scope: 변수가 선언된 메서드 내부
  • lifetime: 프로그램이 해당 메서드를 제어하는 동안

타입 변환, 캐스팅 그리고 타입 프로모션

타입 변환

자료형이 자기 자신보다 큰 자료형에 넣게되면 그건 타입 변환이 필요없고 자동 캐스팅으로 변환이 된다.

하지만 큰 자료형이 작은 자료형으로 가려면 타입 변환이 필요하다.

int a = 1;
byte b = (byte)a; //변형이 오지 않음

만약 작은 자료형의 데이터 범위를 넘어선 변수가 타입 변환을 한다면?
데이터의 손실이나 변형이 올 수 있다.

int a = 10000;
byte b = (byte)a; //byte 최대 범위를 넘었기 때문에 변형이 생긴다.

타입 프로모션

타입 캐스팅과 반대로 크기가 더 작은 자료형을 더 큰 자료형에 대입하는 것을 의미한다. 예를 들어, byte타입의 데이터를 int타입에 대입하는 경우이다.

이 경우에는 데이터 손실이나, 변형이 오지 않음으로 캐스팅할 때 처럼 명시적으로 적지 않아도 자동으로 변환이 가능하다.

그냥 캐스팅과 프로모션은 같이 생각해야 안 헷갈릴 것 같다 구분하는 필요를 안둬도 될것 같은? 느낌

이 타입 변환들은 primitive 타입에서만 사용되는게 아니라 참조 타입에서도 가능하다.
부모클래스로의 타입변환은 자동적으로 가능하지만 자식클래스로의 타입변환은 타입캐스팅이 필요하다. 하위 클래스는 상위 클래스의 필드나 메소드를 물려받음으로 하위 타입의 객체를 상위 타입으로 바꾼다고 해서 데이터의 손실이나 변형이 일어나진 않으니까 말이다.

1차 및 2차 배열 선언하기

배열은 하나의 동일 타입을 여러개 담을 수 있다.

[]로 감싸진 0으로 부터 시작하는 index가 배열의 길이나 순서를 나타낸다.

배열도 type에 따라 기본 값(default value)로 채워진다. (명시적으로 값을 넣어주지 않는다면)

참조 타입의 경우 Null

1차원 배열

int[] a = new int[5]; //[0,0,0,0,0] 로 초기화
int[] b = {1,2,3,4,5}; // 명시적으로 값을 넣어주면 이대로 초기화가 된다.

int[] c = new int[5];
//값 넣기 이렇게도 가능하다.
c[0] = 1;
c[1] = 2;
c[2] = 3;
c[3] = 4;
c[4] = 5;

1차원 배열은 이렇게 선언하면 된다.

2차원 배열

2차원 배열은 행렬(matrix)를 생각하면 된다.

int[행][렬] = new int[행][렬]

가령 3행 3열의 행렬이 이렇게 있다고 가정하면

1 2 3
4 5 6
7 8 9

자바로 표현되는 선언식은 아래와 같다.

int[][] arr = { {1,2,3}, {4,5,6}, {7,8,9} };

지정된 값 말고 동적으로 추가하는 느낌이라면

int[][] arr = new int[3][3];

for(int i = 0; i < 3; i++) {
    for(int j = 0; j < 3; j++) {
        arr[i][j] = 넣을 변수값
    }
}

하면된다.

타입추론, var

타입 추론이란 데이터 타입을 코드에 명시하지 않아도, 컴파일 단계에서 컴파일러가 타입을 유추해 정해주는 것을 뜻한다. 1.5버전 부터 추가된 Generic 이나 자바 8에서 추가된 람다에서 타입추론이 사용된다. 그리고 자바10 에서는 이러한 타입추론을 사용하는 var 이라는 Local Variable Type-Inference 가 추가되었다.

아직 많이 사용할지는 잘 모르겠다.

  • foreach
    • 그냥 우리가 리스트 열거할 때 쓰던 for문, for(Person person : personList) 문에서 변수 선언할 때 var로 쓰면 편하다. 이클립스나 인텔리제이나 for문 쓸때, 타입 추론하기엔 열거 타입을 정의할 때까지 타입을 직접 작성하거나, 템플릿을 작성해야 해서 난감할 때를 많이 겪어봤을 것이다. 이를 var 키워드가 순식간에 해결해준다.
List<Person> people = new ArrayList<>();
people.add(new Person("a"));
people.add(new Person("b"));
people.add(new Person("c"));

for (var person : people) {
    //....
}

이렇게 작성하면 IDE 에서는 var 키워드를 Person 클래스로 인식할 수 있는 기회가 주어지게 되고, 컴파일할 때도 var 키워드를 Person 으로 변환하게 될 것이다. 타이핑이 정말 간결해진다. Object 타입으로 미리 단정지을 필요도 없다.

728x90

'Java' 카테고리의 다른 글

SOLID 원칙  (0) 2022.08.06
연산자  (0) 2022.08.06
JVM  (0) 2022.08.06
Effective Java 4장 요약  (0) 2022.08.06
728x90

정리하는 겸 유튜브 백기선님 자바 스터디를 보며 다시 상기시키는 공부를 진행한다.

이미 정식 스터디는 끝났지만 늦게 시작하여 블로그에 따로 기록하려고 한다.

📢 1주차

  • JVM이란 무엇인가
  • 컴파일 하는 방법
  • 실행하는 방법
  • 바이트코드란 무엇인가
  • JIT 컴파일러란 무엇이며 어떻게 동작하는지
  • JVM 구성 요소
  • JDK와 JRE의 차이

📌 JVM이란?

JVM이란 Java Virtual Machine(자바 가상 머신) 의 줄임말

일반적인 프로그램은 OS위에서 실행되는데 자바 프로그램 같은 경우에는
OS위의 JVM에서 실행이 된다.

JVM위에서 실행 시키는 이유는 OS에 상관없이 실행시키기 위해서이다.

각기 다른 운영체제에서 실행시키는 이점은 정말 좋다.

📌 JVM의 역할

  • 자바 애플리케이션을 클래스 로더를 통해 읽어 들여 자바 API와 함께 실행하는 것이다.
  • JVM은 자바와 OS사이에서 중개자 역할을 수행하여 OS에 독립적인 플랫폼을 갖게 해준다.
    • OS의 메모리 영역에 직접 접근하지 않고 JVM을 통해 간접적으로 접근한다.
  • JVM은 프로그램 메모리 관리를 알아서 해준다. (GC 자동)

📌 자바 실행과정

  1. 프로그램이 실행되면 JVM은 OS로부터 이 프로그램이 필요로 하는 메모리를 할당받는다. 이 메모리를 할당받고 용도에 맞게 여러 영역으로 나누어 관리한다.
  2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여 자바 바이트코드(.class)로 변환시킨다.
  3. 클래스 로더를 통해 class파일들을 JVM으로 로딩한다.
  4. 로딩된 class 파일들은 Execution engine을 통해 해석된다.
  5. 해석된 바이트코드(.class)는 Runtime Data Areas에 배치되고 수행이 이뤄지게 되는데 여기서 가비지 컬렉터 같은 관리 작업도 수행한다.

image
{: .text-center}

📌 자바 바이트코드

자바 바이트코드(.class)란 JVM이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미한다.

자바 컴파일러(javac)에 의해 변환되는 코드의 명령어 크기가 1바이트라서 자바 바이트코드라고 불리고 있다.

📌 JIT 컴파일러란?

JIT(Just-In-Time) 컴파일러는 바이트코드를 CPU로 직접 보낼 수 있는 명령어로 바꾸는 프로그램이다.

일반적으로 컴파일러는 개발자와 최종 사용자를 위한 애플리케이션의 속도를 결정하는데 있어 핵심이다.

JIT 컴파일러는 성능 최적화를 위해 애플리케이션 런타임을 향상시킬 수 있다.

다른 컴파일러와는 다르게 JIT 컴파일러의 특성은 프로그램이 시작되고 코드를 컴파일 한 후에 JIT컴파일러가 실행된다.

📌 JVM 구성요소

  • 인터프리터
    • 바이트코드(.class)를 해당 하드웨어의 환경에 맞게 변환을 하여 운영체제에 종속적이지 않은 상태를 만들어줌.
  • 클래스 로더
    • 컴파일러가 SSD 내부에 만든 바이트코드 파일을 읽는다.
  • 실행엔진(JIT 컴파일러 포함)
    • 로더에 존재하는 클래스를 읽으면서 static이 붙은것을 판단한다.
    • static이 붙은 클래스가 없다면 그대로 종료
  • GC(Garbage Collector)
    • 더 이상 사용하지 않는 메모리를 자동으로 회수

📌 JRE, JDK 차이

JRE(Java Runtime Environment) : 컴파일된 자바 프로그램을 실행시킬 수 있는 자바 환경

  • JRE는 JVM이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일들과 기타 파일들을 가지고 있다.
  • JRE는 JVM의 실행환경을 구현했다고 할 수 있다.
  • 자바 프로그램을 실행 시키기 위해선 JRE를 반드시 설치해야 한다.
  • 자바 프로그래밍 을 하기 위해선 JDK가 필요하다.

JDK(Java Development Kit) : 자바 프로그래밍에 필요한 컴파일러 등등 포함

  • 개발을 위해 필요한 도구들을 포함한다.
  • JDK를 설치하면 JRE도 같이 설치가 된다.
  • JDK = JRE + @
728x90

'Java' 카테고리의 다른 글

연산자  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
Effective Java 4장 요약  (0) 2022.08.06
TDD Clean Code with Java 12기 3주차  (0) 2022.08.06
728x90

4장 클래스와 인터페이스

이펙티브 자바를 TDD, Clean Code 과정을 수강하면서 들으니 이해가 잘되면서 잘 읽힌다❗

클래스와 멤버의 접근 권한을 최소화

ㅋㅋㅋㅋㅋㅋㅋㅋ 어제 Clean Code 과정에서 뼈저리게 겪었다...

랜덤값 부터 시작해서 어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 차이는 클래스 내부 데이터를 각각 다른 외부 컴포넌트로부터 얼마나 추상화 하였는가가 결정 짓는다.

잘 설계되었다면 구현부, API를 깔끔하게 분리할 수 있다.
MVC 패턴만 사용했어 분리하는게 조금 어려웠다. 그런데 진도 나가면서 점점 발전하는것 같다. 😁

이 챕터에서는 정보 은닉(캡슐화) 의 장점에 대해 설명하고 있다.

  • 시스템 개발 속도를 높인다.
    • 여러 컴포넌트를 병렬로 개발 가능하기 때문
  • 시스템 관리 비용을 낮춘다.
    • 다른 컴포넌트로 교체하는 부담이 적어진다.
  • 성능을 높여주진 않지만, 성능 최적화에는 도움을 준다.
    • 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있음.
  • 소프트웨어 재사용성을 높인다.
  • 큰 시스템을 제작하는 난이도를 갖춰준다.

접근 제어자(private, public, protected)

접근 제어자를 잘 써야한다.
사실 이제껏 private, public 두개만 사용했지 protected는 활용 빈도가 지극히 낮았던게 나의 코드였다.

여기서 말하는 원칙을 준수하려면 모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다.

접근 범위가 좁은 순서

  • private : 멤버를 선언한 가장 바깥의 클래스에서만 접근 가능
  • package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
    • 접근 제한자를 명시하지 않았을 때 적용되는 접근 수준(interface는 public)
  • protected: package-private의 접근 범위를 포함하며 상속받는 하위 클래스에서도 접근할 수 있다.
  • public: 그냥 다 접근 가능

근데 나는 여기중에서 package-private를 쓰지않고 도메인을 더 분리하여 일급 컬렉션을 사용하면서 호출하는식으로 관리하는 것이 더 좋을 것 같다는 생각.

결국 public을 많이 사용하면 할수록 점점 코드가 뚱뚱해진다.

🤷‍♂️ 그렇다는건 또 분리를 해야한다는 것 ❗❗❗

접근 제어자 범위를 늘릴 때,
테스트를 하기 위해서 접근 제어자의 범위를 늘려주는 것은 하나의 방법이 될 수 있겠지만
인터페이스를 사용해서 값을 할당하는 strategy 패턴을 고려하는게 좋다고 생각한다.
이것도 뼈저리게 느낀 이번챕터..

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
public 가변 필드를 갖는 클래스라면 스레드 안전하지 않다. 그 클래스는 이 필드를 통제할 통솔력이 없다. 밖에서도 참조가 가능하기 때문에 불변을 보장할 수도 없다.
상당히 좋지 않은 방식... 그래서 DTO를 사용할 때 다 private로 기본 가져가는것이 이유중 하나일 수 있다.

이 책에서는 상수용public static final 외에는 public 필드를 가져서 안된다고 하는데 내가 생각할때는 이 마저도 남용할 수 있는 우려가 있기 때문에 private로 바꿔서 해당 클래스에서만 사용하게 해야 좋은 것 같다. 사용 빈도가 많다면 Enum을 만들어서 상수를 관리하자.

public 클래스에서는 public 필드 말고 접근제어자 메소드를 사용

이것이 dto에서 쓰는 기본 원칙이다.

class Class {
    public int x;
    public int y;
}

이러면

class Main {
    public static void main(String[] args) {
        Class class = new Class();
        class.x = 1;
    }
}

데이터에 직접 위의 코드처럼 할당이 가능해서 캡슐화의 이점을 사용할 수 없다.
그래서 필드를 private 변경하고 get, set 메서드를 사용한다.

class Class {
    private int x;
    private int y;

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }
}

이렇게 말이다.

불변 필드를 노출하게 되면 원하는 데이터가 정형화되어 나올 수 있는 환경이 파괴되기 때문에 접근제어자를 고민하면서 구현하자❗

728x90

'Java' 카테고리의 다른 글

[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
JVM  (0) 2022.08.06
TDD Clean Code with Java 12기 3주차  (0) 2022.08.06
TDD Clean Code with Java 12기 2주차 피드백  (0) 2022.08.06
728x90

JPA에러

JPA실습을 위해 준비한 그리고 동시에 api를 만들어보기 위해서 실습하던 JPA 미니 프로젝트에서 에러가 발생했다.

스크린샷 2021-08-15 오후 5 48 49
{: .text-center}

스크린샷 2021-08-15 오후 5 48 27
{: .text-center}

오류내용

오류는 바로 JPA metamodel must not be empty! 이다.

분명 나는

@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }

}

스프링부트 실행 application 클래스에 JPAAuditing을 추가해주었다.
그리고 MockBean을 전부 넣어줘서 문제가 없다고 생각했는데 어디가 문제인지 알지 못했었다.

JPA는 엔티티 클래스의 LocalDateTime을 자동으로 관리해주는 Auditing 기능이 있다.

이 자동 삽입되는 기능을 쓰려고 Application 클래스에 @EnableJpaAuditing을 삽입했다.

모든 테스트는 이 Application 클래스가 항상 로드되면서 실행이 되는데,

여기에 Auditing 어노테이션이 등록되어 있어서 모든 테스트가 항상 이 JPA관련된 빈을 필요하고 있는 상태가 되는 것이다.

전체 컨텍스트를 로드하면서 빈을 전체 주입받으면 에러가 발생하지 않았지만,
나의 경우는 Controller 테스트를 하기위해 @WebMvcTest를 사용했는데 이 어노테이션은 JPA에 관련된 것들은 로드하지 않는다.

방법

방법은 크게 두가지가 있다.

나중에 또 발견하게 되면 업데이트를 해야겠다.

Configuration 별도 분리

스터디원중에 예전에 한번 지나가면서 얘기했던 것이 있었는데 그것이 바로 방법 첫번째이다.

따로 config라는 별도의 패키지에 Auditing관련 Configuration Class를 만들어 주는 것이다.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}

이렇게 따로 빼주었다.

JpaMetaModelMappingContext 클래스 MockBean 추가

이 방법은 할때마다 연관되지 않은 Controller를 추가해줄때마다 즉,
@WebMvcTest를 하는 곳이면 그러면서 Auditing엔 관련없는 그런 Controller에는 다 해야 될것이다.

상당히 귀찮은 작업이므로 1방법이 더 좋다.

이 포스팅을 함으로써 느꼈다.

Application 클래스에 별도의 어노테이션을 삽입하지 말고 따로 분리해서 Configuration을 관리해야겠다.

728x90

'JPA' 카테고리의 다른 글

컬렉션과 부가기능  (0) 2022.08.06
준영속 상태의 지연로딩을 해결하는 방법  (0) 2022.08.06
[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Native SQL  (0) 2022.08.05
728x90

토이 프로젝트 관련

JPA 스터디를 하면서 배운 내용을 토대로 하여 Spring Data JPA를 더 유연하게 사용하기 위해 토이프로젝트를 진행했다.

이슈사항이 될 수도 있겠다 싶어서 삽질 아닌 삽질을 하다가 알게된 사실이다.

사실, 계속 궁금했다. 🤔

코드로 보면 아래와 같은 궁금증이었다.

@Test
void findById() {
    memberRepository.save(member);
    Optional<Member> result = memberRepository.findById(1L);

    result.ifPresent(m -> {
        assertThat(m.getName()).isEqualTo("홍길동");
    });
}
@Test
void findAll() {
    memberRepository.save(member);
    List<Member> list = memberRepository.findAll();
    assertThat(list.size()).isEqualTo(0);
}

이렇게 두 메서드 이다.

결과부터 보자면

findById

image

findAll

findAll

결과가 다르다. 아무 sql을 출력해주지 않는다.

이유는 findAll을 하면 해당 테이블에 있는 모든걸 가져와야 하니까 update를 해야한다.

이 트랜잭션 밖에서 무슨일이 벌어져서 해당 테이블에 무슨 변화가 있었는지 모른다.

그래서 실행 전에 update도 발생한다❗

현재 트랜잭션 안에서 벌어진 일도 반영하고 select를 해야 현재 가장 최신 상태의 데이터를 가져오는거니까 동기화를 하는 느낌? 이라고 생각하면 될것같다.

근데 findById는 한건에 대한 데이터고, 그 한 건이 현재 트랜잭션 안에서 변경중인 데이터이다. 그래서 해당 변경 사항은 이미 영속성 컨텍스트가 관리하는 중이었기 때문에 findAll처럼 updateselect가 발생할 필요없이, 영속성 컨텍스트 안에서 캐싱하고 있던 객체를 찾아주기 때문에 updateselect 모두 발생하지 않은것 처럼 보인다. 근데 해당 코드가 테스트가 아니라 일반 애플리케이션 코드였으면 트랜잭션 끝나고 update가 발생한다.

이게 왜 발생한다고 말을 할 수 있냐면 실제로 한 메서드 내의 트랜잭션에서 save를 빼고 다른 트랜잭션에서 save를 해주고 findById만 조회하면 또 잘나오게 된다.

JPA를 사용함에 있어서 트랜잭션 정말 중요하다는걸 또 한번 깨닫는다❗

데이터를 원하는 조건에 맞게 추출하게 로직을 구현하는건 중요하지만 그 전에 트랜잭션 관리가 최우선이다. 앞으로 더 참고해서 개발해보도록 하자 😁

728x90
728x90

재귀 알고리즘

개념을 먼저 익힌 후에 백준 알고리즘을 푸는식으로 해야겠다. 맨땅에 헤딩식으로 하면 나는 잘 이해가 안되는 것 같다. 천천히 하더라도 깊이있게 해보자❗

재귀란?

어떤 사건이 자기 자신을 포함하고 다시 자기 자신을 사용하여 정의될 때 재귀적이라고 한다.

바로 예제로 가보도록 하자

팩토리얼 구하기

재귀의 예시로 팩토리얼이 있다.
n!(팩토리얼) 은 n * n-1 * n-2 * ... 이다.
이것을 그대로 코드로 표현하면 다음과 같다.

import java.util.Scanner;

public class Factorial {
    static int factorial(int n) {
        if(n > 0)
            return n * factorial(n - 1);
        else
            return 1;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        System.out.println(n + "! = " + factorial(n));
    }
}

동작형식을 자세하게 보면 factorial(int n)은 n이 0보다 크면 n * factorial(n - 1) 를 호출하고 아니면 1을 반환한다.

풀어서 써보자.

예를 들어 n값에 3을 넣었다고 가정한다.

  • n = 3
    • 3 * factorial(2);
    • 2 * factorial(1);
    • 1 * factorial(0);
    • return 1

이 순서로 들어가게 될텐데,

맨 마지막의 1부터 1 * 2 * 3 이 되는 것이다.
호출은 n = 3부터 시작했으니 정렬이 되면 3 * 2 * 1이 되게 된다.

이렇게 해서 3! 의 답인 6을 얻게된다.

내가 재귀를 볼때 막힘없이 봐야 이해가 쏙 되는것 같은 느낌 😂

한번에 안읽히면 머릿속에서 무한루프를 도는느낌이다.

항상 끝부터 쭉 나간다음 생각해 보는것이 이해가 잘된다❗

재귀 안썼을 때

static int factorial(int n) {
        int count = 1;

        for(int i = n; i > 0; i--) {
            count *= i;
        }

        return count;
    }

유클리드 호제법

최대공약수를 재귀로 구할 수 있다.

두 정수를 직사각형 두 변의 길이라고 가정하면 최대 공약수를 구하는 문제는 다음과 같아질 수 있다.

직사각형을 정사각형으로 완전히 채우고, 만들 수 있는 정사각형의 가장 긴 변의 길이를 구하자

4와 22 의 최대 공약수를 구해보자고 가정하면

  1. 22 x 4에서 4를 한변으로 하는 정사각형으로 분할한다.
  2. 5개의 4 x 4 정사각형이 생기고 2 x 2 두개가 남는다.
  3. 이렇게 더이상 나눌 수 없는 2 x 2 정사각형이 생겼으므로 2가 최대 공약수이다.
public class Euclid {
    static int solution(int x, int y) {
        if(y == 0) {
            return x;
        }

        return solution(y, x % y);
    }

    public static void main(String[] args) {
        int x = 22;
        int y = 4;
        System.out.println(solution(x, y));
    }
}

단순하게 해석하면 0일때까지 계속 solution 자기 자신을 호출하면서 맨 마지막에 남은 x를 돌려주는 것이다.

재귀 안썼을 때

static int gcd2(int x, int y) {
        while (y != 0) {
            int temp = y;
            y = x % y;
            x = temp;
        }
        return x;
    }

배열 모든 요소의 최대 공약수

public class EucArray {

    static int euc(int x, int y) {
        while (y != 0) {
            int temp = y;
            y = x % y;
            x = temp;
        }
        return x;
    }

    static int eucArray(int[] arr, int index, int length) {
        if(length == 1) {
            return arr[index];
        }

        if(length == 2) {
            return euc(arr[index], arr[index + 1]);
        }

        return euc(arr[index], eucArray(arr, index + 1, length - 1));
    }

    public static void main(String[] args) {
        int[] arr = {3, 6, 8, 12, 15};
        System.out.println(eucArray(arr, 0, arr.length));
    }
}

막상 풀어보니까 이렇게 복잡한거는 재귀보다는 다른 방법이 나아보이는....

728x90

'CS' 카테고리의 다른 글

불 논리 회고  (0) 2022.08.07
HTTP  (0) 2022.08.07
불 논리 정리  (0) 2022.08.07
[알고리즘] 그리디  (0) 2022.08.04

+ Recent posts