인턴을 했을 때 JPA 를 쓰긴 썼었는데 안 쓴거나 마찬가지였던 것 같다ㅠㅠㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

이전에는 아래와 같이 객체를 테이블에 맞춰서 모델링 했었음...

class Job{
    @Id
    private String jobId
    ...
}
class Step{
    @Id
    private String stepId;
    private String jobId;
    ...
}

이렇게 했을 때는 객체를 테이블에 저장하거나 조회할 때는 쉽고 편하다.

하지만 job에서 step을 조회할때, 또는 step에서 job을 조회하는 경우 참조된 객체를 가져올 수 없다.

물론 이 때는 데이터베이스에 저장이 주 기능이어서 딱히 상관없었을 수도 있다,,,

 

이번 토이 프로젝트에서는 JPA를 이용하여 객체지향 모델링을 해 볼건데

이번에는 객체 참조를 통해 관계를 맺도록 해볼것이다.

 

  • build.gradle dependency 추가

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

 

server:
  address: localhost
  port: 8080

spring:
  jpa:
    show-sql: true # API 호출시, SQL 문을 콘솔에 출력
    generate-ddl: true # DDL 정의시 데이터베이스의 고유 기능을 사용 ex) 테이블 생성, 삭제 등
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
  datasource:
    url: jdbc:mysql://localhost:3306/ewhaianDB?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver

 

  • Entity class 생성,,,을 하기 전에 저번에 정의했던 연관관계를 다시 보면 (객체는 참조가 있는 방향으로만 조회가 가능하다.)

    • 회원-글/시간표/강평/댓글: 일대 다 관계

    • 글-댓글: 일대 다 관계

    • 글/댓글-사진: 일대 다 관계

    • 강의-강의평가: 일대 다 관계

    • 시간표-강의:다대 다 관계

      • 시간표에 다른 강의 여러개 넣을 수 있고

      • 같은 강의가 여러 시간표에 들어갈 수 있다

      • --> 다대 다 관계는 관계형 데이터베이스 뿐만 아니라 엔티티에서도 거의 사용하지 않는다고 함

      • --> 따라서 연결 엔티티 강의시간표를 추가해서 일대다 관계+다대일 관계로 만들도록 한다.

      • 강의 시간표에는 담은 강의의 학점 합도 포함하도록 한다.

    • 추가) 즐겨찾기의 경우도 다대다 관계이므로 관계를 쪼개도록 한다.

 

 

 

 

 

 


  • JPA를 사용할 때 양방향 관계에서 주의할 점

@Entity
@Getter @Setter
public class BookStore{
    @Id @GeneratedValue
    private Integer id;
    
    private String name;
    
    @OneToMany(mappedBy="bookStore")
    private Set<Book> books=new HashSet<>();
    
    void add(Book book){
        //book.setBookStore(this); 이 부분을 꼭 설정해주도록
        this.books.add(book);
    }
}
@Entity
@Getter @Setter
public class Book{
    @Id @GeneratedValue
    private Integer id;
    private Sring isbn;
    private String title;
    
    @ManyToOne
    private BookStore bookStore;
}

위와 같이 객체를 정의해두고 테스트 코드를 아래와 같이 돌렸을 때 결과를 확인하면

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoJpaTestApplicationTests{
    @Autowired
    BookStoreRepository bookStoreRepository
    
    @Autowired
    BookRepository bookRepository
    
    @Test
    public void contextLoads(){
        BookStore bookStore=new BookStore();
        bookStore.setName("시애틀 책방");
        bookStoreRepository.save(bookStore);
        
        Book book=new Book();
        book.setTitle("JPA 공부 좀 하면서 쓰세요.");
        bookStore.add(book);
        bookRepository.save(book);
    }
}

--> book DB를 확인하면 bookStore id가 없다. (연관 관계 맵핑이 안되어 있다.)

 

mappedBy가 없을 때에는 Book과 BookStore은 양방향 관계가 아니고 서로 다른 단방향 관계 2개이다.

BookStore에서 Book을 참조하는 OneToMany 관계는 기본적으로 join 테이블(book_store_id - book_id)이 생성된다.

 

여기서 mappedBy="BookStore"를 써주면 양방향 관계가 된다. (OneToMany와 ManyToOne을 하나로 묶어준다.)

--> 이 때 관계의 주인을 Book(Many에 해당하는 쪽)으로 설정한다.

이 경우에는 join테이블이 생기는 것이 아니라 Book과 BookStore 테이블 2개만 생기고,

foreign key를 Book쪽에서 가지고 있게 된다. (Book 테이블에 book_store_id가 있음)

 

즉, mappedBy는 관계의 주인을 알려준다.

= 관계의 주인이 Book이고, Book에서 자기 자신을 BookStore라고 참조하고 있다는 뜻

= 이 경우 관계의 주인인 쪽에 관계가 설정 되어야 데이터베이스에 반영된다.

= add를 할 때 관계의 주인인 Book에 관계를 설정해야 sync가 된다.

 

출처

https://www.youtube.com/watch?v=brE0tYOV9jQ

https://victorydntmd.tistory.com/208

https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-1-i3k0xuve9i

 

'Java (+ Spring)' 카테고리의 다른 글

Java Unit Test: Junit5  (0) 2021.02.10
Spring Boot: Spring Security & 카카오 로그인 API(...ing)  (0) 2020.10.06
Spring: Spring MVC 설정 하기  (0) 2020.07.26
Android: tutorial page  (0) 2020.07.18
Android: Launch screen  (0) 2020.07.16

Spring MVC 프레임워크 Support 추가 후 /web 패키지가 생성되는데, 이 아래에 있는 web.xml 파일의 아래 부분을

  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>*.form</url-pattern>
  </servlet-mapping>

 

이렇게 바꿔줘야 한다.

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

 

톰캣의 web.xml파일은 톰캣의 실행환경에 대한 정보를 담당하는 환경설정 파일이다. 

각종 servlet의 설정과 servlet 매핑, 필터, 인코딩 등을 담당한다.
web.xml은 각 application이 deploy될 때 각 application의 'WEB-INF/web.xml' deployment descripter에 따라서 처리가 된다.


 

일단 서블릿과 웹 페이지 동작에 대해 잘 모르겠어서 관련 내용을 찾아 보았다.

 

서블릿이란 웹 프로그래밍에서 클라이언트의 요청을 처리하고 그 결과를 다시 클라이언트에게 전송하는 서블릿 클래스의 구현 규칙을 지킨 자바 프로그래밍 기술이다. 즉, 클라이언트의 HTTP 요청에 대해 특정 기능을 수행, HTML문서를 생성등의 응답을 하는 인터넷 서버 프로그램이다. 일반적으로 웹 서버는 정적인 페이지만을 제공한다. 따라서 동적인 페이지를 제공하기 위해서 웹 서버는 다른 곳에 도움을 요청하여 동적이 페이지를 작성해야 한다. 동적인 페이지로는 임의의 이미지만을 보여주는 페이지와 같이 사용자가 요청한 시점에 페이지를 생성해서 전달해 주는 것을 의미한다.

 

여기서 웹 서버가 동적인 페이지를 제공할 수 있도록 도와주는 어플리케이션이 서블릿이고, 동적인 페이지를 생성하는 어플리케이션이 Common Gateway Interface이다. CGI는 별도로 만들어 놓은 프로그램에 HTML의 GET/POST 방법으로 클라이언트의 데이터를 환경 변수로 전달하고, 프로그램의 표준 출력 결과를 클라이언트에게 전송한다.

 

서버와 클라이언트는 HTTP request/response로 통신하는데, 서버가 요청을 받으면 정적 자원에 대한 요청일 경우 자원을 반환해주고, 그렇지 않는 경우 CGI 프로그램을 실행하여 해당 결과를 리턴해준다. 이 때, 서버는 CGI 프로그램에게 클라이언트의 요청과 매개 변수를 전달해주고, 결과를 전달 받기 위한 파이프라인을 연결한다. 그래서 CGI 프로그램은 입력에 대한 서비스를 수행하고, 결과를 클라이언트에게 전달하기 위해 결과 페이지에 해당하는 MIME 타입의 컨텐츠 데이터를 웹 서버와 연결된 파이프라인에 출력하여 서버에 전달한다. 서버는 파이프라인을 통해 CGI 프로그램에서 출력한 결과 페이지의 데이터를 읽어 HTTP 응답헤더를 생성하여 데이터를 함께 반환해준다.

 

--> Servlet

  • 클라이언트의 요청에  대해 동적으로 작동하는 웹 어플리케이션 컴포넌트이다.
  • html을 사용하여 요청에 응답한다.
  • 자바 스레드를 이용하여 동작한다.
  • MVC 패턴에서 컨트롤러로 이용된다.
  • HTTP 프로토콜 서비스를 지원하는 javax.servlet.http.HttpServlet 클래스를 상속받는다.
  • UDP보다 속도가 느리다.
  • HTML 변경 시 Servlet을 재컴파일 해야 하는 단점이 있다.

 

이 서블릿을 관리해주는 서블릿 컨테이너가 있다. 서블릿 컨테이너는 서블릿의 생명주기를 관리하고 요청에 따른 스레드를 생성해준다. 또, 클라이언트의 request를 받아주고 response를 보낼 수 있게 웹 서버와 소켓을 만들어 통신을 해준다. 서블릿 컨테이너의 예가 톰캣이다. 톰캣은 실제로 웹 서버와 통신하여 Java Server Page(JSP)와 Servlet이 작동하는 환경을 제공해준다.

 

--> Servlet Container이란

  • 웹 서버와의 통신 지원: 일반적으로 소켓을 만들어서 listen, accept를 함으로써 통신을 하지만, 서블릿 컨테이너는 서블릿과 웹 서버가 쉽게 통신할 수 있도록 API를 제공해준다. 
  • 서블릿 생명주기 관리: 서블릿 컨테이너는 서블릿 클래스를 로딩하여 인스턴스화 하고, 초기화 메소드를 호출하고, 요청이 들어오면 적절한 서블릿 메소드를 호출한다. 또한, 서블릿이 생명이 다 한 순간에는 적절하게 가비지 컬렉션을 진행한다.
  • 멀티 스레드 지원 및 관리: 서블릿 컨테이너는 요청이 들어올 때마다 새로운 자바 스레드를 하나 생성하는데, HTTP 메소드 실행 후에 스레드는 죽는다. 원래는 스레드를 관리해야 하지만 서버가 다중 스레드를 생성 및 운영해주기 때문에 스레드의 안정성에 대해서 걱정하지 않아도 된다.
  • 보안 관리: 서블릿 컨테이너를 사용하면 보안 관련 내용을 서블릿 또는 자바 클래스에 구현하지 않아도 된다. 일반적으로 보안 관리는 xml 서술자에 기록하기 때문에, 보안에  대해 수정할 일이 생기더라도 자바 소스 코드를 수정하여 다시 컴파일 하지 않아도 보안 관리가 가능하다. 

 


 

다시 web.xml 설정으로 돌아가서

톰캣의 ${TOMCAT_HOME}/conf/web.xml 파일에는 3개의 서블릿 매핑이 존재한다.

 

즉 *.jsp, *.jspx와 같은 url 패턴은 JspServlet이 처리하고,

DefaultServlet은 spring Controller mapping과 jsp 패턴에 걸리지 않는 요청 들을 처리한다.

(png, jpg, js, html등 정적인 content)

 

<servlet-mapping>
	<servlet-name>default</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>

...

<servlet-mapping>
	<servlet-name>jsp</servlet-name>
	<url-pattern>*.jsp</url-pattern>
</servlet-mapping>

...

<servlet-mapping>
	<servlet-name>jsp</servlet-name>
	<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

처음에 servlet-mapping을 /로 설정하면서 default 서블릿을 dispatcher 서블릿으로 덮어쓴 것이다.

그런데 이렇게만 해준다면 기존의 jsp 패턴에 걸리지 않던 png, jpg, js, html과 같은 정적인 content를 요청할 때 오류가 날 것이다.

 

.jsp든 .js, .css든 WAS만 구동하는경우 해당 파일에 직접접근하는 경우는 없다. 일단 요청은 톰캣이 모두 받게된다.

그리고 톰캣은 서블릿에 패턴을 통해서 요청을 처리하게되는데 /로 지정하게되면 *.jsp로 오는 요청은 jsp 서블릿이 처리하겠지만,

그 외에는 default 서블릿이 처리를 해야하는데 이 default 설정을 덮어써서 default 서블릿이 작동하지 않으면 문제가 발생한다.

 

이를 해결하기 위해서는 서블릿 필터 등을 통해 정적인 요청은 default 서블릿으로 명시적으로 위임하도록 하는 2가지 방법이 있다.

이 핸들러를 구성하면 dispatcher가 모든 요청을 default servlet으로 전달한다.

 

WebConfig.java

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
public class MvcConfiguration implements WebMvcConfigurer {

  @Bean
  public ViewResolver getViewResolver(){
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/");
    resolver.setSuffix(".html");
    return resolver;
  }

  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
}

 

servlet-context.xml: Default ServletHandler가 Bean으로 등록되며 동작

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">
  <mvc:annotation-driven/>
  <mvc:default-servlet-handler/>
</beans>

 

마지막으로 project structure - articats에 들어가서 필요한 라이브러리를 추가해주도록 한다.

- Spring_5_2_3_RELEASE

- Spring_MVC_5_2_3_RELEASE

 

참고

https://lng1982.tistory.com/97

https://multifrontgarden.tistory.com/145

 

 

안드로이드 어플리케이션 최소 접속일 경우 튜토리얼 페이지를 띄우도록 한다.

 

 

0
튜토리얼 페이지

 

튜토리얼 페이지는

  1. 슬라이드로 넘어가야 하고
  2. 앱 실행시 최초 한번만 실행되어야 한다

슬라이드 activity를 만들기 위해서 viewPager을 사용하려고 했는데,

https://developer.android.com/training/animation/vp2-migration

 

ViewPager에서 ViewPager2로 이전  |  Android 개발자  |  Android Developers

ViewPager2는 ViewPager 라이브러리의 개선된 버전으로, 향상된 기능을 제공하며 ViewPager 사용 시 발생하는 일반적인 문제를 해결합니다. 앱에서 ViewPager를 이미 사용하고 있는 경우 이 페이지에서 ViewP

developer.android.com

viewPager2가 새로 나왔다고 해서 이걸로 만들어봤다.

 

기존의 viewPager는 PagerAdapter 기반으로,

스크롤 시 instantiateItem()  destroyItem() 메서드가 호출된다 --> 스크롤 할 때 버벅거림

이를 해결하려면 RecyclerView에 PagerSnapHelper로 해결할 수 있는데 복잡함

 

하지만 viewPager2는 RecyclerView기반이므로 따로 PagerSnapHelper를 만들 필요 없다.

(그리고 viewPager2는 final 클래스라서 커스텀 할 수 없음)

 

먼저 dependency를 추가해준다.

implementation "androidx.viewpager2:viewpager2:1.0.0"

 

activity_tutorial.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tutorial_viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  • 여기서는 괜찮았지만, 원래 viewpager2의 layout_width와 layout_height는 match_parent로 해 주어야 에러가 안난다고 한다. 그래서 만약에 크기를 지정해서 사용하고 싶다면 viewpager2를 다른 뷰 그룹으로 감싸주고 상위 뷰의 크기를 변경하도록 해야한다.

tutorial_1~4.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/tutorial_1" />

</RelativeLayout>

 

tutorial_5.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/splash_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:opacity="opaque">

    <ImageView
        android:layout_width="95dp"
        android:layout_height="128dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="196dp"
        android:layout_marginBottom="316dp"
        android:gravity="center"
        android:src="@drawable/splash_icon_color" />

    <TextView
        android:layout_width="110dp"
        android:layout_height="100dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="334dp"
        android:layout_marginBottom="264dp"
        android:fontFamily="@font/baskervillebold"
        android:text="@string/app_name"
        android:textColor="@color/colorPriary"
        android:textSize="37sp" />

    <Button
        android:id="@+id/tutorial_end_button"
        android:layout_width="184dp"
        android:layout_height="48dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="408dp"
        android:layout_marginBottom="184dp"
        android:background="@drawable/rounded_button"
        android:text="@string/app_start"
        android:textColor="@color/colorPriary"
        android:typeface="sans" />
</RelativeLayout>

 

그 다음 Adapter를 만들어줘야 하는데,

viewPager2는 RecylerView.Adapter 또는 FragmentStateAdapter로 설정할 수 있다.

 

Adapter란 하나의 객체로서, 뷰와 그 뷰에 올릴 데이터를 연결한다.

즉, 데이터의 원본을 받아 관리하고, 뷰가 출력할 수 있는 형태로 데이터를 제공한다.

 

암튼 두 어댑터의 차이점을 알아보니 아래와 같음

  • RecyclerView.Adapter
    • viewPager가 PagerAdapter로 뷰를 통해 페이징 하는 경우
    • pagerAdapter: if you only want to have code that isn't going to be reused
  • FragmentStateAdapter
    • viewPager가 FragmentPagerAdapter로 정해진 소수의 fragment를 통해 페이징 하는 경우
    • viewPager가 FragmentStatePagerAdapter로 많은 수/다이나믹한 프래그먼트를 통해 페이징 하는 경우
    • fragment--Adapter: you can use fragments inside this. if you want to have fragments that are going to be used in other parts of your code ~~

그래서 FragmentStateAdapter으로 ScreenSlidePagerAdapter을 구현하도록 했다.

package com.app.priaryapp.adapter;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import com.app.priaryapp.view.tutorial.TutorialFragment;

import java.util.ArrayList;
import java.util.List;

public class ScreenSlidePagerAdapter extends FragmentStateAdapter {

    private List<Integer> layouts = new ArrayList<>();

    public ScreenSlidePagerAdapter(FragmentActivity fragmentActivity, List<Integer> layouts) {
        super(fragmentActivity);
        this.layouts = layouts;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return TutorialFragment.newInstance(layouts.get(position));
    }


    @Override
    public int getItemCount() {
        return layouts.size();
    }
}

 

ScreenSlidePagerAdapter는 알아서 TutorialFragment를 객체를 만든다. 이 때 주의할 점은 button객체와 onclick을 붙일 때 fragment 안에 있는 것이므로 fragment에서 해야하지 뒤에 나오는 TutorialActivity에서 하면 Null Pointer Error가 나와서 안됨

package com.app.priaryapp.view.tutorial;

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.app.priaryapp.MainActivity;
import com.app.priaryapp.R;

public class TutorialFragment extends Fragment {

    private Button buttonTutorialEnd;

    private TutorialFragment() {
    }

    public static TutorialFragment newInstance(int page) {
        TutorialFragment tutorialFragment = new TutorialFragment();
        Bundle args = new Bundle();
        args.putInt("tutorial_page", page);
        tutorialFragment.setArguments(args);
        return tutorialFragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        int page = this.getArguments().getInt("tutorial_page");
        View view = inflater.inflate(page, container, false);

        if (page == R.layout.tutorial_5) {
            buttonTutorialEnd = view.findViewById(R.id.tutorial_end_button);
            buttonTutorialEnd.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
                    getActivity().finish();
                }
            });
        }

        return view;

    }
}

 

참고: recycler view가 넘겨질 때 애니메이션을 설정하기 위해 ZoomOutPageTransformer 추가

package com.app.priaryapp.util;

import android.view.View;

import androidx.annotation.NonNull;
import androidx.viewpager2.widget.ViewPager2;

public class ZoomOutPageTransformer implements ViewPager2.PageTransformer {

    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    @Override
    public void transformPage(@NonNull View page, float position) {
        int pageWidth = page.getWidth();
        int pageHeight = page.getHeight();

        if (position < -1) {
            page.setAlpha(0f);
        } else if (position <= 1) {
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                page.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                page.setTranslationX(-horzMargin + vertMargin / 2);
            }

            page.setScaleX(scaleFactor);
            page.setScaleY(scaleFactor);

            page.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
        } else {
            page.setAlpha(0f);
        }
    }
}

 

그 다음 TutorialActivity를 만든다.

package com.app.priaryapp.view.tutorial;

import android.os.Bundle;

import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;

import com.app.priaryapp.R;
import com.app.priaryapp.util.ZoomOutPageTransformer;
import com.app.priaryapp.adapter.ScreenSlidePagerAdapter;

import java.util.ArrayList;
import java.util.List;

public class TutorialActivity extends FragmentActivity {

    private ViewPager2 tutorialViewPager;
    private FragmentStateAdapter tutorialPagerAdapter;
    private List<Integer> tutorialLayouts;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tutorial);

        tutorialViewPager = findViewById(R.id.tutorial_viewpager);
        tutorialViewPager.setPageTransformer(new ZoomOutPageTransformer());
        tutorialLayouts = new ArrayList<>();
        tutorialLayouts.add(R.layout.tutorial_1);
        tutorialLayouts.add(R.layout.tutorial_2);
        tutorialLayouts.add(R.layout.tutorial_3);
        tutorialLayouts.add(R.layout.tutorial_4);
        tutorialLayouts.add(R.layout.tutorial_5);

        tutorialPagerAdapter = new ScreenSlidePagerAdapter(this, tutorialLayouts);
        tutorialViewPager.setAdapter(tutorialPagerAdapter);

    }
}

 

마지막으로 앱을 처음 깔았을 때 단 한번만 튜토리얼 페이지가 나오도록 해야 하기 때문에

SharedPreference로 튜토리얼 페이지가 실행됐는지 저장해두고, false인 경우에만 튜토리얼 페이지를 실행하도록 한다.

package com.app.priaryapp;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;

import com.app.priaryapp.view.splash.SplashActivity;
import com.app.priaryapp.view.tutorial.TutorialActivity;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this, SplashActivity.class);
        startActivity(intent);

        SharedPreferences sharedPreferences = getSharedPreferences("checkFirstAccess", Activity.MODE_PRIVATE);
        boolean checkFirstAccess = sharedPreferences.getBoolean("checkFirstAccess", false);


        if (!checkFirstAccess) {
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putBoolean("checkFirstAccess", true);
            editor.apply();

            Intent tutorialIntent = new Intent(MainActivity.this, TutorialActivity.class);
            startActivity(tutorialIntent);
            finish();
        }
    }
}

 

 

참고로 안드로이드 프로젝트는 MVP 패턴(Model-View-Presenter)으로 패키지를 나누는게 좋다고 한다.

 

MVP 패턴

 

  • View: Activity/Fragment가 해당되며 실제 뷰에 대한 직접적인 접근을 담당한다. 뷰에서 발생하는 이벤트는 직접 핸들링 할 수 있으나 Presenter에 위임하도록 한다. 위임하는 방법은 액티비티가 뷰 인터페이스를 구현해서 Presenter에서 코드를 만들 인터페이스를 갖도록 하면 된다고 한다. 이렇게 하면 특정 뷰와 결합되지 않고 가상 뷰를 구현하여 간단한 유닛 테스트를 할 수 있다.
  • Presenter: MVC 모델에서 컨트롤러 역할과 비슷한데, 뷰에 연결되는 것이 아니라 인터페이스로 연결된다. 뷰와 모델 사이의 자료 전달 역할을 한다.
  • Model: 앱 데이터를 말하는데, 데이터 그 자체 외에도 데이터를 관리, 수집, 수정 등을 하는 부분이다.

splash screen 가이드는 2가지가 있다.

  • placeholder UI: 로딩 전에 UI 등을 placeholder로 미리 보여줄 때 (로딩화면 같은...?)
  • branded launch: 앱 시작할 때 순간적으로 브랜드 로고 등을 띄우는 경우

스플래시 화면

 

그 중 두번째에 해당하기 때문에 branded launch를 사용할건데,

처음에는 handler를 사용하지 않고 style에서 xml을 list-layer로 만든 뒤 activity_main의 background로 설정 함으로써

일정 시간의 지연 시간 없이 (사용자가 기다릴 필요 없이) 앱이 준비가 다 되면 넘어가도록 하려고 했는데,

 

아쉽게도 splash 화면에 text가 들어가서 위 방법으로 진행하지 못했다.

text를 꼭 넣고 싶다면 텍스트를 png로 저장해서 비트 맵으로 넣거나 vectorDrawable을 사용하면 되긴 한데,,,

위 두 가지 방법을 쓰느니 그냥 layer xml로 만들고 handler로 intent를 보내기로 했다.

 

  • MainActivity.java

Main에서 SplashActivity를 부른다. 

package com.app.priaryapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent=new Intent(this, SplashActivity.class);
        startActivity(intent);
    }
}

 

 

 

  • SplashActivity.java --> manifest 파일에 추가하는 것 잊지 말 것

지금은 뒤에 더 추가하지 않았지만, 메인 화면 말고 다른 화면을 띄울거면 run() 함수 내부의 finish() 전에

Intent intent= new Intent(getApplicationContext(), 다른 클래스.class);

startActivity(intent);

위 코드를 추가함으로써 다른 화면으로 넘어갈 수 있다. finish()는 현재 액티비티를 종료하는 함수이다.

아래 코드에서 스플래시 화면은 뒤로가기를 할 수 없도록 onBackPressed() 함수를 추가해 두었다.

package com.app.priaryapp;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;

public class SplashActivity extends Activity {
    private final int SPLASH_TIME=1000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                finish();
            }
        }, SPLASH_TIME);
    }

    @Override
    public void onBackPressed(){

    }
}

 

 

 

https://material.io/design/communication/launch-screen.html#usage

 

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

material.io

 

나중에 시간 남으면 스플래시 화면에 애니메이션 넣을 예정인데 참고 자료 - xml로 애니메이션 설정하네,,, 신기하다

https://developer.android.com/training/animation

 

애니메이션 및 전환  |  Android 개발자  |  Android Developers

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates. Last updated 2020-06-20 UTC.

developer.android.com

화면에 도형을 그리는 어플리케이션을 만들어 사용자에게 배포하였다고 가정하자. 이미 도형에 대한 클래스 모델링이 잘 되어 있기 때문에 Shape이라는 인터페이스는 이미 배포되어 있다. 그리고 몇몇 도형들 역시 배포된 상태다. 소프트웨어 내부에서는 이미 배포된 jar 파일을 통해 도형들을 생성해서 사용하고 있다.

이 상황에서 고객이 새로운 도형을 만들어 달라고 요청해 왔다. 필요한 도형은 Circle과 Triangle이다. 이 파일들은 개발자에 의해서 개발되었고 사용자에게 배포되었다. 이 때 이미 동작 중인 어플리케이션은 종료할 수 없다. 이런 경우에 어플리케이션을 중단시키지 않아도 클래스들을 로드할 수 있는 동적 로더가 활용될 수 있다.

 

Shape 인터페이스는 이미 배포된 상태이다. 여기서 사용자가 원하는 Circle과 Triangle을 각각의 이름으로 된 jar 파일을 배포하였다.

여기서 사용자가 어플리케이션을 종료하지 않고 이들 jar 파일들을 로드하려면 동적 Jar 로드 기능이 탑재되어 있어야 한다. 이를 구현한 것이 DynamicJarLoader 클래스이다.

 


DynamicJarLoader 클래스는 객체 생성자를 통해서 Jar 파일이 들어 있는 폴더 경로(String jarPath)를 입력 받는다.

이후 load(jarFileName : String) 메소드를 통해서 Jar 파일 이름을 입력해주면 라이브러리가 로드된다.

특히 DynamicJarLoader 클래스는 내부에 있는 loaderMap을 통해서 여러 이름의 라이브러리를 동시에 로드할 수 있다. 따라서 이 객체 하나 만으로도 여러 라이브러리 파일을 로드하고, 로드된 라이브러리들을 통해서 객체를 생성할 수 있다. 리턴되는 boolean 값은 라이브러리 로드가 성공했는지 여부를 알려준다.

 

unload(jarFileName : String) 메소드는 이미 로드된 라이브러리를 메모리에서 내리거나, 새로운 버전의 라이브러리를 열기 위해서 기존에 이미 열려진 라이브러리를 닫아야 할 경우에 호출하는 메소드이다.

 

이미 라이브러리가 로드되어 있다면 열린 라이브러리를 통해서 객체를 새로 생성할 수 있어야 한다. newInstance() 메소드는 클래스 이름(className)을 통해서 객체를 로드할 수 있도록 구현된 메소드이다. newInstance() 메소드는 두 개가 있는데, 하나는 className만으로 객체를 생성하도록 하고, 나머지 하나는 jar 파일 이름까지 입력해서 보다 정확한 객체를 생성하도록 한다.

 

public class DynamicJarLoader {

    private String jarPath;
    private Map<String, URLClassLoader> loaderMap = new HashMap<String, URLClassLoader>();

    public DynamicJarLoader(String jarPath){
        this.jarPath = jarPath;
        this.jarPath.replaceAll("\\\\", "/");
        if(this.jarPath.endsWith("/") == false) this.jarPath = this.jarPath + "/";
    }

    public boolean load(String jarFileName){
        if(loaderMap.containsKey(jarFileName) == true) unload(jarFileName);

        String jarFilePath = jarPath + jarFileName;
        File jarFile = new File(jarFilePath);

        try {
            URL classURL = new URL("jar:" + jarFile.toURI().toURL() + "!/");
            URLClassLoader classLoader = new URLClassLoader(new URL [] {classURL});
            loaderMap.put(jarFileName, classLoader);
            return true;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    public boolean unload(String jarFileName){
        URLClassLoader loader = loaderMap.get(jarFileName);
        if(loader == null) return true;

        try {
            loader.close();
            return true;
        } catch (IOException e) {
            return false;
        }
        finally{
            loaderMap.remove(jarFileName);
        }
    }

    public Object newInstance(String jarFileName, String className){
        URLClassLoader loader = loaderMap.get(jarFileName);
        if(loader == null) return true;

        try {
            Class<?> clazz = loader.loadClass(className);
            return clazz.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            return null;
        }
    }

    public Object newInstance(String className){
        for(String each : loaderMap.keySet()){
            Object object = newInstance(each, className);
            if(object != null) return object;
        }

        return null;
    }
}

'Java (+ Spring)' 카테고리의 다른 글

Spring: Spring MVC 설정 하기  (0) 2020.07.26
Android: tutorial page  (0) 2020.07.18
Android: Launch screen  (0) 2020.07.16
Reflection을 활용한 범용 toString() 함수 만들기  (0) 2020.07.08
enum의 활용법 - 계산기  (0) 2020.07.07

기본적으로 Java에서는 모든 클래스가 toString() 함수를 지원한다. 따라서 라이브러리에 선언되어 있는 클래스들은 .toString() 함수만 호출하면 해당 클래스 내부에 있는 값들을 출력해 낼 수 있다. 하지만 사용자가 직접 선언한 클래스는 toString() 함수 호출만 가지고 내부 변수에 가지고 있는 값들을 확인하기가 어렵다.

 

예를 들어 아래와 같은 클래스를 선언하면

class TestClass{
    short s = 1;
    int i = 10;
    long l = 20;
    String name = "nameString";
    Long L = new Long(100);
    List<String> list = new ArrayList<String>();
    Map<String, String> map = new HashMap<String, String>();

    public TestClass(){
        list.add("string1");
        list.add("string2");
        list.add("string3");

        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");
    }
}

이후 toString() 함수를 호출해보면

TestClass tc = new TestClass();
System.out.println(tc.toString()); // MyPattern.reflection.TestClass@15db9742

패키지 경로에 따라서 조금씩 다를 수는 있지만 기본적으로는 내부에 있는 변수들의 값이 나오지는 않는다. 그래서 새로 클래스를 정의할 때마다 toString() 함수를 재정의(override) 할 필요가 있다. 하지만 Java 프로그래밍을 하면서 엄청나게 많은 클래스들을 만들텐데 새로 만드는 클래스마다 각 클래스에 필요한 toString() 함수를 재정의 하기는 너무 힘든 일이다.

 

따라서 재정의를 하지 않으면서 범용으로 사용할 toString 클래스를 만들어 보자.

 

public class ToString {  
    public static String toString(Object object){
        Field []fields = object.getClass().getDeclaredFields();

        String str = "{";

        for(Field field : fields){
            field.setAccessible(true);
            try {
                String type =               
                    field.getType().toString().
                    substring(field.getType().toString().lastIndexOf(".") + 1);

                if(field.getType().toString().startsWith("class ") &&           
                   !field.getType().toString().startsWith("class java.lang.")){
                   // primitive 타입이나 라이브러리에서 제공하는 객체가 아닐때 toString을 재귀적 호출
                   str += type + " " + field.getName() + 
                          toString(field.get(object)) + " ";
                }
                else{
                   str += type + " " + field.getName() + ":" + 
                          field.get(object) + " ";   
                }
            } catch (IllegalArgumentException e) {
            } catch (IllegalAccessException e) {
            }
        }
        return str + "} ";
    }
    public static void main(String[] args) {
        AllTypes types = new AllTypes();
        System.out.println(ToString.toString(types));
        /* 실행 결과
        {short s:1 int i:1 long l:1 Short S:0 Integer I:0 Long L:0 InnerClass inner{int a:100 long b:200 String name:innerClass }  List list:[] Map map:{} } 
        */
    }
}

getClass()는 Reflection에 활용되는 클래스 객체를 가지고 오는 함수이다. 즉, object 객체는 .class 객체를 가지고 있는데, 객체의 본래 타입에 따라서 .class 객체는 모두 다르다. getDeclaredFields() 함수는 클래스 객체로부터 선언된 모든 Field 객체들을 배열 형태로 가져오는 함수다. .class 객체가 객체의 본래 타입에 따라 다르기 때문에 getDeclaredFields() 함수를 통해 나오게 되는 Field[]도 모두 다르다.

 

for문 안에 setAccessible(true) 함수 호출되는 부분의 경우는, 만약 Field가 public이 아닌 경우 캡슐화로 인하여 그 값에 접근하는 것이 불가능한데 Reflection은 이 함수를 통해서 접근이 가능하도록 만들 수 있다.(이 부분은 아직도 이슈가 많은 부분이라 프로그래머 마다 찬반 의견이 다르다. 하지만 유용성에 대해서는 아무도 부인하지는 못할 것이다.)

 

  • String getType() : type에 대한 클래스 객체(Class object)를 반환하는 함수  --> 타입을 반환하긴 하는데 primitive 타입이 아닌 경우 타입이 장황하게 출력될 수 있다. 예를 들어 Long 타입의 경우 class java.lang.Long 이라고 출력됨. 따라서 위와 같이 줄이는 부분을 만든다.
  • String getName() : 필드명을 String 객체로 반환하는 함수
  • Object get(object) : Field가 값으로 가지고 있는 객체를 반환하는 함수

약간 덧붙이자면, 일단 field가 null인 경우 동작하지 않을 수 있으므로 null 체크도 들어가는게 좋다.

그리고 배열의 경우 위의 경우에서 모두 벗어나기 때문에 제대로 출력이 안될 수 있다. 이 부분은 필요시 직접 처리하기 바란다.

만일 ToString 클래스를 사용해야 하는 것이 불편하고, toString()을 재정의 해서 사용하고 싶다고 할 때에도 이 ToString 클래스는 유용하다. 클래스마다 toString() 함수를 재정의 할 때 다음과 같이 동일하게 해주면 된다.

@Override
public String toString() {
    return ToString.toString(this);
}

 

 

 

'Java (+ Spring)' 카테고리의 다른 글

Spring: Spring MVC 설정 하기  (0) 2020.07.26
Android: tutorial page  (0) 2020.07.18
Android: Launch screen  (0) 2020.07.16
Java 라이브러리(.jar) 동적 로딩: DynamicJarLoader  (0) 2020.07.08
enum의 활용법 - 계산기  (0) 2020.07.07

Java에서의 enum 타입

enum Type{ // abstract class
	ADD, // public static final class ADD extends Type{}
	SUB; // public static final class SUB extends Type{}
}

기본적으로 enum은 추상클래스이고, 그 하위에 선언된 각 열거형 변수는 실제로는 변수가 아니라 enum 의 타입을 상속받은 하위 클래스이다. 이 하위 클래스는 외부에 노출되어 있고 생성할 필요가 없으며 런타임에 교체가 불가능하므로 public static final 타입을 갖는다.

 

enum은 기본적으로 추상 클래스이나, 다른 클래스로부터 상속을 받지 못한다.

하지만 interface는 상속을 받을 수 있다. Enum에 함수가 들어갈 수 있는지 완전 몰랐다,,,,

interface Interface{
    public void api();
}


enum Type implements Interface{
    ADD{
        public void api(){ System.out.println("ADD api"); }
    },
    SUB{
        public void api(){ System.out.println("SUB api"); }
    };
}

이 특성을 이용해서 디자인 패턴에 나오는 수많은 패턴들을 enum을 통해 구현할 수 있다.

enum이 public static final 이라는 점은 Singleton과 유사한 특성을 지니고 있다.

따라서 중복 생성이 안되면서도 일반 클래스 기능을 할 수도 있고, 필요한 때에는 enum의 특성을 활용할 수도 있다.

이 강력한 특성 때문에

Singleton 패턴, Abstract Factory 패턴, Factory Method 패턴, State 패턴, Strategy 패턴, Visitor 등 다양한 패턴의 enum 버전이 있다.

 

이 외에도 enum은 추상 클래스이므로 함수를 선언할 수 있고, 추상 메소드 또는 static 메소드의  선언도 가능하다.

enum Type{
    ADD,
    SUB;
    public void api(){ System.out.println("api()"); }
}
enum Type{
    ADD{
        public void api(){ System.out.println("ADD api"); }
    },
    SUB{
        public void api(){ System.out.println("SUB api"); }
    };
    abstract public void api();
    public static int size(){ return Type.size(); }
}

 

또한 클래스이므로 생성자도 선언할 수 있어야 한다.

enum Type{
    ADD(0),// 생성자에게 필요한 인자는 () 안에 넣는다.
    SUB(1); //
    int value;
    private Type(int value){ this.value = value; }   // 이것이 생성자.
    public int value(){ return value; }
}

enum 클래스를 인덱스로 활용할 수 있도록 value 라는 int 형 값을 생성시에 인자로 받도록 했다. Java의 enum은 클래스이기 때문에 C언어에서 처럼 값으로 활용할 수가 없다. 대신에 이처럼 생성자를 통해 인자로 받은 값을 가지고 있다가 값이 필요한 경우 value() 함수를 호출함으로써 값으로도 사용이 가능하다.

이때 생성자가 private인 것에 주목해야 한다.

enum은 외부에서 생성이 불가능하기 때문에 생성자는 항상 private으로 선언해 주어야 한다.

 

enum은 클래스이므로 하위 타입들도 모두 toString() 함수를 가지고 있다. 그 결과 값은 기본적으로 자신의 선언된 이름과 같다. ADD.toString()은 "ADD" 값이 결과값이다. 이러한 특성은 매우 유용한데, 특히 데이터베이스에 저장할 때 그렇다.

DB의 가독성 측면에서 문자열을 활용한 경우에 문자열을 입력받아 타입을 리턴하도록 하면

코드 상에서 문자열 대신 enum을 활용할 수 있으므로 코딩이 편리해진다.

public static Type getTypeByString(String str){
    for(Type each : values()){
        if(each.toString().equals(str)){
            return each;
        }
    }
    return null;
}

 

enum 타입이 제공하는 기본 함수로 enum의 순서를 알 수 있는 함수가 있다.

public int ordinal();

위 ordinal() 이라는 함수인데 이 함수는 선언된 enum의 하위 타입이 몇 번째 순서로 선언되었는지를 알 수 있다.(순서의 시작 값은 0이다.) 가령 위에서 선언한 ADD 타입의 경우 0이 리턴되고, SUB의 경우 1이 리턴된다. 이 특성을 이용하면 enum을 인덱스로도 활용이 가능하다.

 

Java에서의 enum은 열거형의 특성과 클래스의 특성을 함께 가지고 있다는 장점이 있다. toString() 함수를 가지고 있다는 것만으로도 디버깅을 얼마나 쉽게 만들어 주는지 모른다. 그 밖에도 데이터 베이스와의 연동, switch-case 문에 대한 활용, 인터페이스 상속을 활용한 디자인 패턴 등 다양한 곳에 활용할 수 있다.

 


그렇다면 enum 타입을 배열의 인덱스로 활용하는 방법을 알아보자

class Distance{
    private int east;
    private int west;
    private int north;
    private int south;

    public void setEast(int east){ this.east = east; }
    public void setWest(int west){ this.west = west; }
    public void setNorth(int north){ this.north = north; }
    public void setSouth(int south){ this.south = south; }

    public int getEast() { return east; }
    public int getWest() { return west; }
    public int getNorth() { return north; }
    public int getSouth() { return south; }
}

위와 같은 클래스를 보면

모두 같은 타입(int)의 변수를 선언하고 있고, 모두 getter와 setter를 제공하고 있음을 알 수 있다. 그리고 동서남북을 나타내는 변수들이므로 서로 연관성도 있어 보인다. 이런 경우에는 enum 타입을 하나 선언해서 동서남북을 가리키는 서브 타입을 만들고 이것을 인덱스로 활용하는 방법을 사용하면 중복이 제거된다.

enum Direction{
    EAST,
    WEST,
    NORTH,
    SOUTH;
}

class Distance{
    // 각 방향별로 선언되었던 변수를 하나의 배열로 선언
    private int distance[] = new int[Direction.values().length];
    
    public int get(Direction direction){ 
        return distance[direction.ordinal()]; 
    }
    public void set(Direction direction, int value){ 
        distance[direction.ordinal()] = value; 
    }
}

enum 하위 타입의 개수를 알아내는 방법은 보통 Type.values().length 를 이용한다. .values() 함수는 enum 하위 타입들을 배열화하여 리턴해주는 함수이다. .length는 잘 알다시피 배열의 길이를 나타내는 배열 객체의 변수이다. 그리고 이들은 앞서 말한 바와 같이 컴파일 타임에 결정되기 때문에 배열의 개수 초기화에도 사용할 수 있다.

get함수에서는 enum 타입인 Direction  하위 타입을 어떻게 인덱스 숫자로 바꾸느냐가 중요한데, JAVA에서는 ordinal() 이라는 함수를 통해 숫자로 바꿀 수 있다. int ordinal() 함수는 해당 서브 enum 타입의 정의 순서에 따른 숫자를 리턴한다. 가령 EAST.ordinal() = 0, WEST.ordinal() = 1......와 같은 방식이다. 이것은 배열의 인덱스 값과 같은 값이다. 따라서 전혀 문제 없이 사용할 수 있다.

 

 

 

https://effectiveprogramming.tistory.com/entry/enum%EC%9D%98-%ED%99%9C%EC%9A%A9%EB%B2%95?category=660010

 

enum의 활용법

C언어에서 enum은 단순히 상수형 변수 역할에 지나지 않았다. 하지만 Java에서는 매우 다른 특성들을 지니고 있다. 이 특성들 중에는 특별한 것들도 있어서 기존과는 다른 여러 방식으로 enum을 활�

effectiveprogramming.tistory.com

 

+ Recent posts