티스토리 뷰
1. this와 this()
this와 this()는 괄호 하나 다를 뿐인데 전혀 다른 의미를 갖고있다.
결론부터 말하자면, this는 객체 자신을 가리키고, this()는 한 클래스 내에서 한 생성자에서 다른 생성자를 호출할 때 사용된다.
this
public class Person {
private String name; //인스턴스 변수
private int age;
private String sex;
public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
}
코드를 보면, Person 생성자 내에서 'this.객체 내의 인스턴스 변수 = 매개변수' 형식으로 this 키워드가 사용되고 있는 것을 볼 수 있다.
this 키워드는 생성자의 매개변수와 클래스의 인스턴스 변수의 이름이 같을 때, 생성자 내에서 인스턴스 변수를 가리키기 위해 사용된다.
예를 들어, this.name = name은 생성자의 매개변수 name을 클래스의 인스턴스 변수 name에 할당한다는 것을 나타낸다.
+) static 메서드에서는 this 키워드를 사용하지 못한다고 한다.
this()
public class Person {
private String name;
private int age;
private String sex;
// 첫 번째 생성자
public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 두 번째 생성자: 위의 생성자를 호출하여 초기화함
public Person() {
this("김땡땡", 23, "Female"); // this()를 사용하여 다른 생성자 호출
}
// 세 번째 생성자
public Person(String name) {
this(name, 23, "Female"); // this()를 사용하여 다른 생성자 호출
}
}
코드를 보면, 두 번째 생성자는 매개변수 없이 호출되지만, this()를 사용하여 같은 클래스의 첫 번째 생성자를 호출하고 있다. 이렇게 함으로써, 매개변수가 없는 생성자를 호출하는 대신, 매개변수가 있는 생성자를 호출하고 기본 값을 전달할 수 있다.
또한 세 번째 생성자처럼 이름(name)만을 받아와서 인스턴스를 초기화할 수도 있다.
따라서, this()를 사용하여 생성자를 호출하면 (클래스 내의 다른 생성자를 호출) 코드의 재사용성을 높일 수 있다.
2. Java의 Generic 타입
Java의 Generic(제네릭)은 클래스, 인터페이스, 메소드를 정의할 때 타입(Type)을 파라미터로 사용할 수 있게 해주는 기능이다.
쉽게 말하자면, 자바에서 제네릭은 마치 상품을 포장하는 상자에 비유할 수 있다. 상품이 무엇인지에 따라 상자의 라벨을 다르게 붙일 수 있는데, 이 라벨이 바로 제네릭에서 말하는 '타입 파라미터'이다.
예를 들어서 설명해보면,
제네릭을 사용하지 않는 경우
class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
모든 종류의 객체를 담을 수 있는 'Object' 타입의 상자를 사용하게 되면, 상자에서 무언가를 꺼낼 때마다 그것이 무엇인지 확인(형변환)해야 한다.
즉, 상품을 여러 종류의 상자에 넣어야 하는데, 모든 상자가 '모든 종류의 상품을 담을 수 있는 상자'라고 라벨링이 되어 있다면, 특정 상품을 찾을 때 상자마다 열어보고 확인하는 번거로운 과정을 거쳐야 한다는 것이다.
그러나, 제네릭을 도입하게 되면
제네릭을 사용하는 경우
class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
코드를 보면 'Box' 클래스에 '<T>'라는 타입 파라미터가 추가된 것을 볼 수 있는데, 이것은 위의 Object와 달리 '이 상자는 T 타입의 물건만 담을 수 있음' 이라는 라벨과 같다. 실제 사용 시에는 'T'를 'Integer'나 'String'과 같은 구체적인 타입으로 지정해주면 된다.
사용 예시
public class Main {
public static void main(String[] args) {
// Integer 타입의 Box 생성
Box<Integer> integerBox = new Box<>();
integerBox.set(123); // Box에 Integer 값 저장
Integer intValue = integerBox.get(); // Box에서 Integer 값 꺼내기
System.out.println("Integer Value: " + intValue);
// String 타입의 Box 생성
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics"); // Box에 String 값 저장
String stringValue = stringBox.get(); // Box에서 String 값 꺼내기
System.out.println("String Value: " + stringValue);
}
}
'Box' 클래스를 사용해 두 가지 다른 타입의 객체를 저장하고 꺼내는 예시이다.
이렇게 외부 클래스에서 제네릭 클래스를 생성할 때 <> 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주면 된다.
+) 제네릭 메서드
제네릭 메소드는 메소드 선언에서 타입 파라미터를 명시하여, 호출 시 다양한 타입에 대응할 수 있게 하는 메소드이다.
선언 방법은 메소드 반환 타입 바로 앞에 <> 안에 타입 파라미터를 명시하면 된다. 이 타입 파라미터는 메소드 내부에서 타입 이름처럼 사용할 수 있다.
예를 들어보면,
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
// 제네릭 메소드 추가
public <U> U inspect(U u) {
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
return u;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
// 제네릭 메소드 Integer
System.out.println("<U> returnType: " + integerBox.inspect(3).getClass().getName());
// 제네릭 메소드 String
System.out.println("<U> returnType: " + integerBox.inspect("ABCD").getClass().getName());
}
}
inspect 메소드는 제네릭 타입 U를 사용하여, 이 메소드가 호출될 때 어떤 타입의 객체든 받을 수 있다.
즉 제네릭은 상자에 라벨을 붙여서, 어떤 타입의 객체만을 담을 수 있게 하는 기능이다. 이를 통해 타입 안전성을 높이고, 형변환의 번거로움을 줄여준다.
3. final, static, static final
final
'final' 키워드는 '최종적인' 이라는 의미로, 선언된 필드, 메소드, 또는 클래스를 변경할 수 없게 만든다.
즉, 한 번 저장되면 수정이 불가능하다.
- 필드에 사용될 경우
final int MAX_VALUE = 10;
필드에 사용될 경우엔 변수에 할당된 값을 변경할 수 없다. 만약 참조 타입 변수에 사용된다면, 참조하는 객체 자체를 변경할 수 없지만 객체 내부의 상태는 변경할 수 있다.
final List<String> list = new ArrayList<>();
list.add("Hello"); // 가능
list = new ArrayList<>(); // 컴파일 에러
- 메소드에 사용될 경우
public final void display() {
System.out.println("This method cannot be overridden");
}
메소드에 사용될 경우엔 메소드를 오버라이딩할 수 없게 한다. 그러나 오버로딩은 가능하다.
- 클래스에 사용될 경우
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
클래스에 사용될 경우엔 클래스를 상속할 수 없게 한다. 즉, 다른 클래스가 이 클래스를 확장할 수 없다.
나아가, 위 코드에선 모든 필드가 'final'이므로, 객체가 생성된 후에는 이 필드들의 값이 변경될 수 없으며, 객체의 상태 (이름과 나이)를 변경하고 싶다면 새로운 'ImmutablePerson' 객체를 생성해야 한다.
static
'static' 키워드는 '정적인'이라는 의미로, 주로 필드나 메소드에 사용된다. 'static'으로 선언된 멤버는 인스턴스에 속하지 않고 클래스에 속하며, 모든 인스턴스가 공유한다.
즉, 객체 생성 없이 사용할 수 있는 필드와 메소드를 생성하고자 할 때 사용된다.
- 필드에 사용될 경우
public static int counter = 0;
필드에 사용될 경우, 클래스 필드(변수)가 되며, 모든 인스턴스에 의해 공유된다. 클래스가 메모리에 로드될 때 생성되고, 프로그램이 종료될 때까지 남아있는다.
- 메소드에 사용될 경우 (정적 팩토리 메소드)
public static Person create(String name, int age, String sex) {
return new Person(name, age, sex);
}
Person personWithFactoryMethod = Person.create("김땡땡", 24, "female);
메소드에 사용될 경우, 클래스 메소드가 되며, 인스턴스를 사용하지 않고도 호출할 수 있다. 위의 코드에서 볼 수 있듯이, 따로 객체를 생성하지 않고도 Person.create로 호출하는 것을 볼 수 있다.
static final
'static final'은 위의 두 키워드가 결합된 형태로, 클래스 레벨의 상수를 정의하는 데 사용된다. 이러한 상수는 클래스가 로드될 때 한 번만 생성되고, 프로그램 실행 동안 변경될 수 없다.
예를 들어,
public static final double PI = 3.14159;
이렇게 원주율과 같이 프로그램 내에서 공유되는 변경 불가능한 상수를 정의할 때 사용된다. 이 상수에 접근하기 위해서는 클래스 이름을 통해 접근하면 된다.
4. super, super()
자바에서 'super' 키워드는 부모 클래스를 참조하는데 사용되며, 주로 필드 또는 메소드에 접근할 때와 부모 클래스의 생성자를 호출할 때인 두가지 상황에서 사용된다. 이 두 가지 사용법은 'super' 필드 접근자와 'super()' 생성자 호출로 구분할 수 있다.
super
자식 클래스에서는 'super' 키워드를 사용하여 직접적으로 부모 클래스의 필드나 메소드에 접근할 수 있다.
이 방법은 특히 자식 클래스가 부모 클래스의 메소드를 오버라이딩했을 때, 오버라이드된 메소드 내에서 부모 클래스의 원본 메소드에 접근하고자 할 때 유용하다.
class Animal {
void makeSound() {
System.out.println("동물 소리");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // "동물 소리"
System.out.println("멍멍");
}
}
예를 들어, 부모 클래스인 Animal이 있고, 이를 상속받는 자식 클래스인 Dog가 있다고 가정해본다면, Animal 클래스에는 모든 동물이 내는 소리를 나타내는 makeSound 메소드가 있고, Dog 클래스에는 이 메소드를 오버라이드하여 "멍멍"이라는 소리를 내는 makeSound 메소드가 있다. 하지만 Dog의 makeSound 메소드에서도 부모 클래스의 makeSound 메소드를 호출하여 "동물 소리"를 먼저 출력하고 싶다면, super를 사용할 수 있다.
super()
자식 클래스의 인스턴스를 생성할 때, 자바는 먼저 해당 인스턴스의 부모 클래스 부분을 초기화 한다. 이 과정에서 자식 클래스의 생성자에서 'super()'를 사용하여 특정 부모 클래스의 생성자를 명시적으로 호출할 수 있다.
단, super() 호출은 자식 클래스 생성자의 첫 줄에서만 사용할 수 있으며, 명시적으로 호출하지 않을 경우 컴파일러는 자동으로 부모 클래스의 기본 생성자를 호출한다.
class Parent {
Parent() {
System.out.println("Parent 기본 생성자 호출");
}
Parent(String message) {
System.out.println("Parent 매개변수 있는 생성자 호출: " + message);
}
}
class Child1 extends Parent {
Child1() {
super(); // 명시적으로 부모 클래스의 기본 생성자 호출
System.out.println("Child1 생성자 호출");
}
}
class Child2 extends Parent {
Child2() {
super("Hello from Child2"); // 부모 클래스의 매개변수 있는 생성자 호출
System.out.println("Child2 생성자 호출");
}
}
class Child3 extends Parent {
// super() 호출 없음. 컴파일러가 자동으로 부모 클래스의 기본 생성자를 호출합니다.
Child3() {
System.out.println("Child3 생성자 호출");
}
}
위 코드를 통해, super() 호출은 자식 클래스 생성자의 첫 줄에서만 사용될 수 있으며, 명시적으로 호출하지 않을 경우 자바 컴파일러가 부모 클래스의 기본 생성자를 자동으로 호출하는 방식을 볼 수 있다.
5. SOLID 원칙
SOLID 원칙은 객체 지향 프로그래밍과 설계에서 코드의 유지보수성, 확장성 및 재사용성을 개선하기 위해 따라야하는 5가지 기본 원칙을 말한다.
Single Responsibility Principle (SRP, 단일 책임 원칙)
하나의 클래스는 하나의 책임만 가져야 한다는 뜻이다.
즉, 클래스가 변경되어야 하는 이유는 오직 하나뿐이어야 한다는 것이다. 변경의 이유가 한가지라는 의미는 해당 클래스가 여러 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 의해서만 있어야 한다는 것이다.
'액터'는 클래스나 모듈에 요구사항을 제시하는 역할을 하는 사람이나 시스템의 부분으로, 예를 들어 사용자 인터페이스(UI)를 관리하는 클래스와 비즈니스 로직을 처리하는 클래스는 각각 다른 '액터'의 요구사항을 충족시키기 위해 존재해야 한다.
이를 통해 시스템의 복잡성을 줄이고, 클래스의 재사용성을 높이며, 변경에 유연하게 대응할 수 있다.
Open-Closed Principle (OCP, 개방 - 폐쇄 원칙)
소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 뜻이다.
즉 기존의 코드를 변경하지 않고도, 시스템의 기능을 확장할 수 이써야 한다.
이 원칙을 따르면 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있다.
Liskov Substitution Principle (LSP, 리스코프 치환 원칙)
프로그램에서 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용해도, 프로그램의 정확성이 변하지 않아야 한다는 뜻이다.
즉 하위 클래스는 상위 클래스와 대체가 가능해야 한다. 다시 말해서 자식 타입을 언제든지 부모 타입으로 변경할 수 있어야 한다는 것이다.
이 원칙을 따르면, 클래스의 계층 구조가 유연해지고, 상속을 통한 재사용성이 증가한다.
Interface Segregation Principle (ISP, 인터페이스 분리 원칙)
하나의 일반적인 인터페이스보다는, 구체적인 여러 개의 인터페이스가 낫다는 뜻이다.
클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않아야 한다. 즉 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공해야 하는 것이다.
이를 통해 클라이언트가 자신에게 필요하지 않은 인터페이스에 의존하지 않도록 하여, 시스템의 결합도를 낮출 수 있다.
Dependency Inversion Principle (DIP, 의존성 역전 원칙)
고수준 모듈은 저수준 모듈에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 뜻이다. 추상화는 세부 사항에 의존해서는 안되며, 세부 사항이 추상화에 의존해야 한다.
의존성 역전 원칙 적용 전
class EmailSender {
void sendEmail(String message) {
System.out.println("이메일 전송: " + message);
}
}
class SmsSender {
void sendSms(String message) {
System.out.println("SMS 전송: " + message);
}
}
class MessageService {
private EmailSender emailSender = new EmailSender();
private SmsSender smsSender = new SmsSender();
void sendEmail(String message) {
emailSender.sendEmail(message);
}
void sendSms(String message) {
smsSender.sendSms(message);
}
}
위 코드에서 MessageService는 EmailSender와 SmsSender에 직접적으로 의존하고 있다. 이 구조에서 새로운 메시지 전송 방법(예: Slack 메시지)을 추가하려면 MessageService 클래스를 수정해야 하므로, 시스템의 유연성이 떨어지고 OCP(개방-폐쇄 원칙)를 위반하게 된다.
의존성 역전 원칙 적용 후
interface MessageSender {
void sendMessage(String message);
}
class EmailSender implements MessageSender {
public void sendMessage(String message) {
System.out.println("이메일 전송: " + message);
}
}
class SmsSender implements MessageSender {
public void sendMessage(String message) {
System.out.println("SMS 전송: " + message);
}
}
class MessageService {
private MessageSender messageSender;
// 의존성 주입을 통한 유연성 확보
public MessageService(MessageSender sender) {
this.messageSender = sender;
}
void send(String message) {
messageSender.sendMessage(message);
}
}
메시지 전송 기능의 추상화를 정의하는 인터페이스 'MessageSender'를 만들고, 'EmailSender'와 'SmsSender'가 이 인터페이스를 구현하도록 했다.
이제 MessageService는 구체적인 메시지 전송 방법(EmailSender, SmsSender)이 아닌, MessageSender 인터페이스에 의존한다. 이로써 MessageService는 저수준 모듈의 세부 사항에 의존하지 않고, 추상화에만 의존하게 된다. 새로운 메시지 전송 방법을 추가하고 싶을 때는 단순히 MessageSender 인터페이스를 구현하는 새로운 클래스를 만들고, MessageService의 생성자를 통해 인스턴스를 주입하기만 하면 된다. 이 방식은 시스템의 결합도를 낮추고, 유연성 및 재사용성을 크게 향상시킨다.
6. 스프링의 의존성 주입 방식
스프링 프레임워크에서 의존성 주입(Dependency Injection)은 애플리케이션의 각종 구성요소 간의 의존 관계를 스프링 컨테이너가 자동으로 연결해주는 기능을 말한다. 이 과정에서 객체의 생성과 그 객체가 필요로 하는 다른 객체 간의 관계 설정이 이루어지게 된다.
이렇게만 말하면 다소 어렵기 때문에, 먼저 의존성에 의미를 알아보면
의존성이란? (Dependency)
의존성이란 어떤 객체가 정상적으로 동작하기 위해 다른 객체나 자원이 필요한 상태를 말한다.
예를 들어, Person 클래스가 Address 클래스의 객체를 필요로 한다면, Person은 Address에 의존하고 있다고 할 수 있다.
의존성 주입이란? (Dependency Injection)
의존성 주입은 의존하는 객체를 외부 스프링 컨테이너에서 생성하여 주입해주는 방식이다. 이를 통해, 객체는 직접 의존 객체를 생성하거나 찾지 않고 주입 받을 수 있게 된다.
의존성 주입에는 여러가지 방식이 있는데 먼저,
필드 주입
@Component
public class Person {
@Autowired
private Address address;
}
필드 주입은 스프링이 직접 클래스의 필드에 의존성을 주입하는 방식이다. 코드는 가장 간결하지만, 후에 변경이 불가능한 final 필드에 사용할 수 없고, 테스트가 어려워질 수 있다.
세터주입
@Component
public class Person {
private Address address;
@Autowired
public void setAddress(Address address) {
this.address = address;
}
}
세터 주입은 세터 메서드를 통해 의존성을 주입받는 방식이다. 이 방법은 의존성을 선택적으로 주입하거나, 실행 시간에 의존성을 변경해야 할 경우 유용하다.
생성자 주입
@Component
public class Person {
private final Address address;
@Autowired
public Person(Address address) {
this.address = address;
}
}
생성자 주입은 객체 생성 시 생성자를 통해 의존성을 주입 받는 방식이다. 가장 권장되는 방식이며, 모든 의존성이 주입되어 있음을 컴파일 타임에 보장한다.
생성자 주입 시 필드에 final 키워드를 사용할 수 있다.
생성자 주입의 장점
불변성
생성자를 통해 주입된 의존성은 final로 선언될 수 있어, 객체가 한 번 생성된 이후에는 변경되지 않는다.
이는 객체의 안전성과 예측 가능성을 높여 준다.
순환 의존성 감지
순환의존성이란 두 객체가 각각 서로를 필드에 포함하여 참조하고 있는 상태를 말한다. 서로가 서로를 참조하고 있게 되면 두 클래스가 서로의 객체를 무한 생성하는 문제가 생긴다.
그러나 생성자 주입을 사용하면 스프링이 순환 의존성을 더 쉽게 감지할 수 있다.
명시적인 의존성
생성자 주입은 모든 의존성이 생성자의 파라미터로 명시되기 때문에, 클래스가 어떤 의존성을 필요로 하는지 쉽게 파악할 수 있다.
생성자 주입 방법을 쓰자!
7. Java Record
Java에서 레코드(Record)는 Java 14부터 프리뷰 기능으로 도입되고, Java 16부터 공식적인 기능으로 제공되기 시작한, 데이터를 담기 위한 새로운 유형의 클래스이다. 레코드는 주로 데이터를 운반하는 데 사용되는 클래스들, 즉 데이터 전송 객체 (DTO), 값 객체, 그리고 튜플을 간결하게 표현하기 위해 설계되었다.
레코드의 특징
불변성
레코드의 모든 필드는 final이다. 레코드에 데이터가 한 번 할당되면, 그 데이터는 변경할 수 없다.
데이터 중심
레코드는 데이터를 저장하고 전달하는 데 중점을 둔다. 이는 데이터의 불변성을 보장하며, 레코드 인스턴스 간의 비교가 내용(content)을 기반으로 이루어진다.
간결성
레코드를 사용하면 같은 목적의 클래스를 훨씬 더 간결하게 표현할 수 있다. 필드 정의, 생성자, getter, equals(), toString() 등의 메서드들이 자동으로 생성된다.
예를 들어 설명해보자면, 먼저
레코드 도입 전
public class MemberFindDto {
private final String name;
private final Part part;
private final int age;
public MemberFindDto(String name, Part part, int age) {
this.name = name;
this.part = part;
this.age = age;
}
public String getName() {
return name;
}
public Part getPart() {
return part;
}
public int getAge() {
return age;
}
public static MemberFindDto of(Member member) {
return new MemberFindDto(member.getName(), member.getPart(), member.getAge());
}
// equals, hashCode, toString 메서드 생략
}
레코드 도입 전에는 DTO를 만들기 위해 클래스를 사용하고, 필요한 필드, 생성자, getter 및 유틸리티 메서드를 명시적으로 정의해야 했다.
이 접근 방식에서는 필요한 모든 메서드들을 수동으로 구현해야 하기 때문에 코드의 양을 증가시키고, 불필요한 반복 작업을 초래할 수 있다.
레코드 도입 후
public record MemberFindDto(
String name,
Part part,
int age
) {
public static MemberFindDto of(Member member) {
return new MemberFindDto(member.getName(), member.getPart(), member.getAge());
}
}
그러나, 레코드를 사용하게 되면 같은 DTO를 훨씬 간결하게 정의할 수 있다.
레코드는 불변의 데이터를 저장하기 위해 간결한 구문을 제공하며, 필드에 대한 getter, equals(), hashCode(), toString() 메서드를 자동으로 생성해 준다.
+ ) 레코드의 기존 클래스와의 차이점이 있다면, getter를 사용할 때 'getFieldName()'이 아니라 'fieldName()'을 사용한다는 점이다.
Person person = new Person("김땡땡", 23);
String name = person.getName(); //일반 class
String name = person.name(); // Record class
'Spring' 카테고리의 다른 글
스프링 키워드 모음 #2 (1) | 2024.04.16 |
---|---|
객체 생성 - 정적 팩토리 메서드와 빌더 패턴 (1) | 2024.04.15 |
스프링 커뮤니티 만들기 #10 -회원탈퇴 시 오류해결 2 (NullpointerException) (0) | 2024.03.03 |
스프링 커뮤니티 만들기 #9 - 회원탈퇴 시 오류해결 (외래키 제약조건) (2) | 2024.03.03 |
스프링 커뮤니티 만들기 #8 북마크 목록 조회 (0) | 2024.03.03 |
- Total
- Today
- Yesterday
- 준영속
- SQL 레벨업
- JPA
- 파이썬
- 비영속
- 스프링 북마크
- 스프링 커뮤니티
- 영속
- SQL
- 북마크
- 백준 파이썬
- 자바 스프링
- 프론트엔드
- 지연로딩
- 웹 MVC
- EnumType.ORDINAL
- 로깅
- 스프링
- 커뮤니티
- SQLD
- 백준
- 인텔리제이
- 스프링부트
- 다이나믹 프로그래밍
- 자바
- 웹MVC
- DP
- 회원탈퇴
- elasticsearch
- 로그아웃
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |