프로젝트를 하다보면 고객의 요청에 따라 0.7 같이 소수점을 표시 해서 보여줘야 하는 경우가 존재한다.
간혹 소수점을 표시하여 처리하는 경우 .5 등의 표시 오류가 발생하는데, 이를 해결하는 방법은 TO_CHAR 와 FM 을 사용하는것이다.
## FM990.99
사용방법 : TO_CHAR(표시할숫자, 'FM990.99')
FM
좌우 9로 치환된 소수점 이상의 공백 및 소수점 이하의 0 제거.
FM 이 없는 경우 : 소수점 이상의 숫자는 공백, 소수점 이하의 숫자는 0으로 표시
9
해당 자리의 숫자. (가변길이)
값이 없는경우 소수점 이상 : 공백 으로 표시.
값이 없는경우 소수점 이하 : 0 으로 표시.
0이거나 숫자가 존재하지 않으면 값을 버림.
0
해당 자리의 숫자. (고정길이)
값이 없는경우 : 0으로 표시.
고정적으로 숫자의 길이를 표시할 경우 사용한다.
숫자의 길이를 맞추고 싶을때 길이만큼 0을 추가.
## 사용 예시
SELECT '0.5' AS A -- 0.05표시
, TO_CHAR('0.05', '999,999') AS B -- 0표시
, TO_CHAR('0.05', '999.999') AS C -- .050표시
, TO_CHAR('0.05', 'FM999.99') AS D -- .05표시
, TO_CHAR('0.05', 'FM990.99') AS E -- 0.05표시
, TO_CHAR('0.05', 'FM900.99') AS F -- 00.05표시
, TO_CHAR('0.05', 'FM990.990') AS G -- 0.050표시
, TO_CHAR('0.05', 'FM990.9900') AS H -- 0.0500표시
FROM DUAL;
public class SingletonTest {
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회 : 호출할 때마다 객체 생성하는지 확인.
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조값이 다른 것 확인.
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
클라이언트의 요청(memberService 요청) 시 동일한 memberService를 반환(요청시마다 새로 생성하는게 아닌 이미 만들어진 객체(동일한 객체)를 반환).
## 싱글톤 방식의 주의점 (공유필드 조심! 항상 무상태 (stateless) 로 설계하기!)
객체 인스턴스를 하나만 생성해서 공유하는 경우, 여러 클라이언트가 하나의 객체를 공유해서 사용하기 때문에 싱글톤 객체는 상태를 유지 (stateful) 하게 설계하면 안된다. -> 무상태 (stateless) 로 설계해야 한다.
특정 클라이언트에 의존적인 필드가 없어야 한다.
특정 클라이언트가 값을 변경할수 있는 필드가 없어야 한다.
읽기만 가능해야 하고 값을 수정하면 안된다.
필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
상태 유지로 설계된 경우 예시.
StatefulService (상태 유지)
package singleton;
public class StatefulService {
private int price; // 상태 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
// 문제점.
this.price = price;
}
public int getPrice() {
return price;
}
}
statefulServiceTest
package singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자 20,000 주문.
statefulService1.order("userA", 20000);
// ThreadB : B 사용자 150,000 주문.
statefulService2.order("userB", 150000);
// ThreadA : 사용자A 가 주문 금액을 조회.
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(150000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
위 테스트 코드 실행 시 A사용자가 주문한 금액을 조회하면 아래와 같은 결과가 나온다.
특정 클라이언트가 공유되는 필드의 값을 변경하여 문제 발생. (공유 필드는 조심해야 함)
무상태로 설계된 경우 예시. (위 상태 유지 코드를 아래와 같이 변경 가능)
package singleton;
public class StatefulService {
public int order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price;
}
}
package singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자 20,000 주문.
int userAPrice = statefulService1.order("userA", 20000);
// ThreadB : B 사용자 150,000 주문.
int userBPrice = statefulService2.order("userB", 150000);
// ThreadA : 사용자A 가 주문 금액을 조회.
System.out.println("price = " + userAPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
## @Configuration 과 싱글톤
AppConfig
@Configuration
public class AppConfig {
// memberService 역할
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// memberRepository 역할
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
// orderService 역할
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// discountPolicy 역할
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfig 에서 로직을 살펴보면 아래와 같은 문제점이 존재한다는걸 확인할 수 있다.
@Bean memberService() 시 new MemoryMemberRepository() 호출
@Bean orderService() 시 new MemoryMemberRepository() 중복 호출
위 내용 관련하여 테스트해서 확인 진행
MemberServiceImpl 에 테스트용 메서드 생성.
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
// 테스트용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
OrderServiceImpl 에 테스트용 메서드 생성.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// 테스트용
public MemberRepository getMemberRepository() {
return memberRepository;
}
}