[JAVA] https로 접근했는데 sendRedirect http로 리다이렉트 되는 현상

[JAVA] https로 접근했는데 sendRedirect http로 리다이렉트 되는 현상

response.sendRedirect(“/test/index.jsp”);

위와 같이 sendRedirect 했을 때 https로 리다이렉트되지 않고 http로 리다이렉트 되는 현상 해결방법.

실제로 백엔드 자바 단(WAS 단)에서 request 객체의 scheme이나 isSecure 값을 가져왔을 때 SSL이 적용되지 않은 상태로 인식되는 현상이다.

확인코드

System.out.println(“protocol: “ + request.getProtocol());
System.out.println(“port : “ + request.getServerPort());
System.out.println(“scheme : “ + request.getScheme());
System.out.println(“isSecure: “ + request.isSecure());

결과

protocol: HTTP/1.1

port : 80

scheme : http

isSecure: false

1. WAS가 톰캣인 경우 설정방법

웹서버(아파치 웹서버) – WAS(Tomcat) 구조에서, 웹서버에는 SSL 인증서가 입혀져 있고, WAS(Tomcat)에는 SSL 인증서가 입혀져 있지 않은 경우.

웹서버를 통해 HTTPS 프로토콜로 접속하더라도 WAS에는 인증서가 입혀져 있지 않으므로, 백엔드 자바 단에서는 HTTP 프로토콜 및 80 포트로 접근했다고 인식되는 문제이다.

톰캣의 server.xml 파일의 Connector 태그에 proxyPort=”443″ scheme=”https” 설정을 추가하면 된다.

<Service name=”Catalina”>

    <Connector proxyName=”example.com” proxyPort=”443″ scheme=”https” URIEncoding=”UTF-8″ port=”8000″ protocol=”HTTP/1.1″ redirectPort=”8443″ />

    <Engine defaultHost=”localhost” name=”Catalina”>

        <Host appBase=”webapps” autoDeploy=”true” name=”localhost” unpackWARs=”true” xmlNamespaceAware=”false” xmlValidation=”false”> <Context docBase=”” path=”/” reloadable=”false” /></Host>

    </Engine>

</Service>

참고사이트 : https://hulint.tistory.com/47

2. WAS가 오라클 웹로직(Weblogic)인 경우 설정방법

웹서버(아파치 웹서버 또는 OHS) – WAS(Weblogic) 구조에서, 웹서버에는 SSL 인증서가 입혀져 있고, WAS(Weblogic)에는 SSL 인증서가 입혀져 있지 않은 경우.

웹서버를 통해 HTTPS 프로토콜로 접속하더라도 WAS에는 인증서가 입혀져 있지 않으므로, 백엔드 자바 단에서는 HTTP 프로토콜 및 80 포트로 접근했다고 인식되는 문제이다.

(1) Weblogic 설정 변경

Weblogic 콘솔에 접속해서 해당 웹 어플리케이션의 Weblogic Plugin Enabled 체크박스를 체크처리하면 된다.

Weblogic 콘솔 접속

도메인명 -> configuration 탭 -> Web Applications 탭 -> Weblogic Plugin Enabled 체크하기

참고사이트 1 : https://doohans.github.io/ohs_weblogic_https_scheme/

참고사이트 2 : https://www.python2.net/questions-1070920.htm

 

(2) 웹서버 설정 변경 검토

만약 위의 Weblogic 설정 변경으로 해결되지 않으면 웹서버의 설정도 확인해봐야 한다.

웹서버가 아파치일 경우 X-Forwarded-Proto 값을 헤더에 설정해야 하고, 웹서버가 OHS일 경우 WLProxySSLPassThrough On 설정이 필요하다.

(2-1) 웹서버가 아파치 웹서버인 경우

웹서버가 아파치일 경우 X-Forwarded-Proto 값을 헤더에 설정해야 한다.

<VirtualHost *:443>

    ServerName www.myapp.org

    ProxyPass / http://127.0.0.1:8080/

    RequestHeader set X-Forwarded-Proto

    https RequestHeader set X-Forwarded-Port 443

    ProxyPreserveHost On

    … (SSL directives omitted for readability)

</VirtualHost>

참고사이트 : https://idkook.tistory.com/75

(2-2) 웹서버가 OHS인 경우

여기서 OHS 란 Oracle HTTP Server의 약자로 아파치처럼 웹서버의 일종이다. 이 문제에 대한 해결책은 두 가지가 있다.

1) obj.conf 파일에 아래의 파라미터를 추가하기

WLProxySSL=”ON”

WLProxySSLPassThrough=”ON”

이어서 BIG IP Load Balancer에서 WL-Proxy-SSL=true 를 설정해야 한다.

2) WLProxySSL=”ON” WLProxySSLPassThrough=”ON” 등 플러그인의 파라미터나 WL-Proxy-SSL = true 헤더를 추가하지 않고도

BIG IP Loadbalancer의 “Rewrite Redirect” 옵션을 통해 문제를 해결할 수도 있다.

참고사이트 : https://support.oracle.com/knowledge/Middleware/2215493_1.html

3. 스프링을 사용하는 경우

스프링(Spring)에서 sendRedirect 시 http로 리다이렉트 되는 경우. redirectHttp10Compatible 관련 설정을 추가하면 된다고 한다.

(1) InternalResourceViewResolver 클래스를 사용하는 경우

<beans:bean p:order=”2″ class=”org.springframework.web.servlet.view.InternalResourceViewResolver”>

    <beans:property name=”viewClass” value=”org.springbyexample.web.servlet.view.tiles2.DynamicTilesView” />

    <beans:property name=”prefix” value=”/WEB-INF/views/” />

    <beans:property name=”suffix” value=”.jsp” />

    <beans:property name=”redirectHttp10Compatible” value=”false” />

</beans:bean>

(2) UrlBasedViewResolver 클래스를 사용하는 경우

<beans:bean p:order=”1″ id=”viewResolver” class=”org.springframework.web.servlet.view.UrlBasedViewResolver”>

    <beans:property name=”viewClass” value=”org.springframework.web.servlet.view.tiles2.TilesView” />

    <beans:property name=”redirectHttp10Compatible” value=”false” />

</beans:bean>

참고사이트 : https://iamfreeman.tistory.com/entry/Spring-mvc%EC%97%90%EC%84%9C-redirect-https-http-redirect-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0

[SpringBoot] 인텔리제이(IntelliJ)에서 스프링부트(Spring Boot) 시작하기

[SpringBoot] 인텔리제이(IntelliJ)에서 스프링부트(Spring Boot) 시작하기

1. 스프링부트 프로젝트 생성

https://start.spring.io/ 에 접속한다.

 

Project : Gradle Project 선택

Language : Java 선택

Spring Boot : 2.4.3 선택

Project Metadata

Group : com.회사명 (ex : com.thkmon)

Artifact : 원하는패키지명 (ex : bootbasic)

Name : 원하는패키지명 (ex : bootbasic)

Description : 프로젝트 설명 (빈값 가능)

Project name : com.회사명.패키지명 (ex : com.thkmon.bootbasic)

Packaging : Jar

Java : 11

우측의 Dependencies 목록에는 [ADD] 버튼을 이용해서 [Spring Web] 항목을 추가한다.

하단의 [GENERATE] 버튼을 클릭하면 압축파일을 다운받을 수 있다.

다운받은 압축파일을 적당한 위치에 압축 해제한다. (ex : C:\intellij_workspaces\bootbasic)

2. 프로젝트 열기

인텔리제이 상단메뉴 [File] – [Open] 으로 스프링부트 프로젝트 폴더를 연다.

OOOApplication.java 파일 (ex : BootbasicApplication.java 파일)을 열고 main 메서드 좌측의 초록색 화살표 클릭 – [Run OOOAplication main()] 항목을 클릭해서 스프링부트를 기동시킨다.

스프링부트 내부에 톰캣이 내장되어 있으며, 정상적으로 기동한 경우 주소는 http://localhost:8080 이 된다.

스프링부트 기동 시 Unnecessarily replacing a task that does not exist is not supported.  Use create() or register() directly instead.  You attempted to replace a task named ‘BootbasicApplication.main()’, but there is no existing task with that name. 오류가 발생할 경우.

인텔리제이 상단 메뉴 [File] – [Settings] 에 들어간다. 좌측 메뉴 [Build, Execution, Deployment] – [Gradle] 을 선택하고, 우측 화면의 Build and run using 과 Run tests using 콤보박스 값을 두 개 다 IntelliJ IDEA 로 변경한다. 이후 다시 재기동해본다.

[iOS] NSString URL 인코딩

[iOS] NSString URL 인코딩

NSString *encodedStr = [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]];
NSLog(@”encodedStr : %@”, encodedStr);

[iOS] objective-c WKWebView 예제 (objective-c WKWebView boilerplate code)

[iOS] objective-c WKWebView 예제 (objective-c WKWebView boilerplate code)

내가 만든 것은 아니고 어느 일본인이 개발해둔 것이다.

이 코드가 나를 살렸다…

파일명 일괄변경 프로그램, 파일명 일괄수정 툴 (DarkNamer)

파일명 일괄변경 프로그램, 파일명 일괄수정 툴 (DarkNamer)

DarkNamer.

파일명을 간편하게 일괄수정, 일괄변경할 수 있는 전설의 프로그램…

직관적인 인터페이스로 쉽게 사용할 수 있다.

백업 차원에서 이 포스트에도 파일 첨부해놓는다.

출처 : https://blog.naver.com/darkwalk77/70027450806

[JAVA] 자바에서 파일 날짜 변경, 시간 변경

[JAVA] 자바에서 파일 날짜 변경, 시간 변경

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainClass {

    public static void main(String[] args) {
        String filePath = “C:\\test\\0001.png”;
        File file = new File(filePath);

        try {
            String pattern = “yyyy-MM-dd HH:mm:ss”;
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
            Date timeToSet = simpleDateFormat.parse(“2021-02-01 10:00:00”);

            FileTime time = FileTime.fromMillis(timeToSet.getTime());

            // 만든 날짜 변경
            Files.setAttribute(file.toPath(), “creationTime”, time);
            
            // 수정한 날짜 변경
            Files.setAttribute(file.toPath(), “lastModifiedTime”, time);
            
            // 엑세스한 날짜 변경
            Files.setAttribute(file.toPath(), “lastAccessTime”, time);

        } catch (IOException e) {
            e.printStackTrace();

        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

참고사이트 : http://oliviertech.com/ko/java/how-to-update-the-date-time-of-a-file-using-java/

[Apache] TLS 1.0 또는 TLS 1.1이 사용되었으며, 서버에서 TLS 1.2 이상을 사용해야 합니다.

[Apache] TLS 1.0 또는 TLS 1.1이 사용되었으며, 서버에서 TLS 1.2 이상을 사용해야 합니다.

 

크롬, 엣지 등 최신 브라우저에서 2020년부터 SSL(TLS 1.2) 버전 미만만 지원하는 사이트에 대해 경고 메시지가 표시되고 있다.

경고메시지 내용

연결의 보안이 완벽하지 않음

이 사이트에서는 오래된 보안 설정을 사용하므로 개인정보(예: 비밀번호, 메시지 또는 신용카드)를 이 사이트로 전송할 경우 정보가 유출될 수도 있습니다.

이 사이트를 로드하는 데 사용된 연결에는 TLS 1.0 또는 TLS.1.1이 사용되었으며, 이러한 TLS는 지원이 중단되어 향후 사용 중지될 예정입니다. 사용 중지된 후에는 사용자가 이 사이트를 로드할 수 없습니다. 서버에서 TLS 1.2 이상을 사용해야 합니다.

1. 사이트 접속 방법

단순히 이 사이트에 접속하려면 하단의 “사이트주소(안전하지 않음)(으)로 이동”을 클릭하면 된다.

2. 아파치 웹서버 설정 방법

서버에서 TLS 1.2 이상을 지원하도록 하려면 아파치(Apache) 웹서버 설정파일을 수정해야 한다. 아파치 웹서버의 conf 폴더에 httpd.conf 파일 또는 ssl.conf 파일이 있다. 

httpd.conf에 SSL 설정을 입력한 경우에는 httpd.conf를 수정하고, 다른 파일(예를 들어 ssl.conf)에 SSL 설정을 입력한 경우에는 해당 파일을 수정하면 된다.

[AS-IS]

SSLProtocol -All +TLSv1

[TO-BE]

SSLProtocol -All +TLSv1 +TLSv1.1 +TLSv1.2 +TLSv1.3

이후 아파치 웹서버를 재기동하면 된다.

참고사이트 : https://it.goodinfoall.com/8

[JAVA] 자바 쓰레드를 만드는 2가지 방법 (Thread, Runnable)

[JAVA] 자바 쓰레드를 만드는 2가지 방법 (Thread, Runnable)

자바에서 쓰레드(Thread)를 만드는 방법은 크게 2가지가 있다.

첫번째는 Thread 클래스를 상속받아서 사용하는 방법이다.

두번째는 Runnable 클래스를 구현해서 사용하는 방법이다. Thread가 아닌 다른 클래스를 상속받고 싶을 때, Runnable 클래스를 구현해서 사용한다. 자바에서 상속은 1개 클래스까지만 허용하기 때문이다.

1. Thread 클래스를 상속받아서 사용

public class MainClass {

    public static void main(String[] args) {
        MainClass mainCls = new MainClass();
        mainCls.main();
    }
    
    public void main() {
        Thread thread = new myThread();
        thread.start();
    }
    
    public class myThread extends Thread {
        @Override
        public void run() {
            System.out.println(“myThread”);
        }
    }
}

또는 아래와 같이 간략하게 써도 된다.

    Thread myThread = new Thread() {
        @Override
        public void run() {
            System.out.println(“myThread”);
        }
    };
    myThread.start();

2. Runnable 클래스를 구현해서 사용

Thread가 아닌 다른 클래스를 상속받고 싶을 때, Runnable 클래스를 구현해서 사용한다. 자바에서 상속은 1개 클래스까지만 허용하기 때문이다.

아래 예제에서는 Something 이라는 클래스를 상속받았다.

public class MainClass {

    public static void main(String[] args) {
        MainClass mainCls = new MainClass();
        mainCls.main();
    }
    
    public void main() {
        Thread thread = new Thread(new myRunnable());
        thread.start();
    }

   
    public class myRunnable extends Something implements Runnable {
        @Override
        public void run() {
            System.out.println(“myRunnable”);
        }
    }
}

따로 클래스 상속이 필요없다면 아래와 같이 간략하게 써도 된다.

    Thread myRunnable = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(“myRunnable”);
        }
    });

    myRunnable.start();

[Tomcat] 톰캣 java.io.IOException: 파일 이름, 디렉터리 이름 또는 볼륨 레이블 구문이 잘못되었습니다

[Tomcat] 톰캣 java.io.IOException: 파일 이름, 디렉터리 이름 또는 볼륨 레이블 구문이 잘못되었습니다

1월 27, 2021 9:24:32 오전 org.apache.catalina.startup.Bootstrap initClassLoaders
심각: Class loader creation threw exception
java.io.IOException: 파일 이름, 디렉터리 이름 또는 볼륨 레이블 구문이 잘못되었습니다
    at java.io.WinNTFileSystem.canonicalize0(Native Method)
    at java.io.WinNTFileSystem.canonicalize(WinNTFileSystem.java:428)
    at java.io.File.getCanonicalPath(File.java:618)
    at java.io.File.getCanonicalFile(File.java:643)
    at org.apache.catalina.startup.ClassLoaderFactory.createClassLoader(ClassLoaderFactory.java:169)
    at org.apache.catalina.startup.Bootstrap.createClassLoader(Bootstrap.java:201)
    at org.apache.catalina.startup.Bootstrap.initClassLoaders(Bootstrap.java:146)
    at org.apache.catalina.startup.Bootstrap.init(Bootstrap.java:256)
    at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:453)

톰캣 기동시 java.io.IOException: 파일 이름, 디렉터리 이름 또는 볼륨 레이블 구문이 잘못되었습니다 오류가 발생한 경우.

파일 경로에 한글이 포함되어 있는 경우, 특히 maven을 사용 중인데 윈도우 계정이 한글명인 경우 위 오류가 발생할 수 있다고 한다.

내 경우에는 VM arguments 부분의 -Dcatalina.base 경로를 적을 때, 여는 쌍따옴표는 있는데 닫는 쌍따옴표가 빠져서 오류가 발생했다.

꼭 한글 문자열이 포함되는 경우가 아니더라도 파일 경로가 잘못되면 발생될 수 있는 에러다.

[JAVA] java.io.NotSerializableException: org.apache.catalina.session.StandardSessionFacade

[JAVA] java.io.NotSerializableException: org.apache.catalina.session.StandardSessionFacade

여러 대의 WAS 간 세션 클러스터링을 적용한 경우, 세션(session)의 어트리뷰트에 담는 Object가 직렬화를 구현(implements Serializable)하지 않은 경우 java.io.NotSerializableException 오류가 발생한다.

2021. 1. 21 오후 10:12:10 org.apache.catalina.ha.session.DeltaManager requestCompleted
심각: Unable to serialize delta request for sessionid [A5A5A91419C7543592867E3FBAFE85FA]
java.io.NotSerializableException: org.apache.catalina.session.StandardSessionFacade
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1164)
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1518)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1483)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1400)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1158)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:330)
    at org.apache.catalina.ha.session.DeltaRequest$AttributeInfo.writeExternal(DeltaRequest.java:407)
    at org.apache.catalina.ha.session.DeltaRequest.writeExternal(DeltaRequest.java:300)
    at org.apache.catalina.ha.session.DeltaRequest.serialize(DeltaRequest.java:314)
    at org.apache.catalina.ha.session.DeltaManager.serializeDeltaRequest(DeltaManager.java:584)
    at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:967)
    at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:935)
    at org.apache.catalina.ha.tcp.ReplicationValve.send(ReplicationValve.java:537)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendMessage(ReplicationValve.java:524)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendSessionReplicationMessage(ReplicationValve.java:506)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendReplicationMessage(ReplicationValve.java:419)
    at org.apache.catalina.ha.tcp.ReplicationValve.invoke(ReplicationValve.java:343)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:445)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1137)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:637)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:662)

대표적으로 String 은 implements Serializable 되어있기 때문에 아래와 같이 써도 문제없다(위 오류와 관계없다).

String str = “value”;

session.setAttribute(“key”, str);

cf) String.class

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    (중략)

}

물론 WAS에 세션 클러스터링을 적용하지 않는 경우에는 세션에 어떤 Object를 담아도 위 오류가 발생하지 않는다.

그래도 이러한 상황을 고려해서 기본적으로 세션 어트리뷰트에 세팅하는 Object들은 implements Serializable 로 직렬화 처리를 해주는 것을 권장한다.

참고사이트 : https://okky.kr/article/575327 

[Tomcat] 톰캣7 세션 클러스터링 방법

[Tomcat] 톰캣7 세션 클러스터링 방법

세션 클러스터링이란 여러 대의 WAS가 세션을 공유하는 것을 말한다.

이렇게 되면 한 대의 WAS가 죽어도 나머지 WAS가 세션을 유지시켜 준다.

참고로 세션은 도메인이 동일해야만 같은 제이세션아이디(세션을 찾아오기 위힌 key에 해당하는 쿠키값)을 갖기 때문에, 세션 클러스터링을 하기 전에 로드밸런싱 작업이 선행되어야 한다.

로드밸런싱이란 쉽게 말해서 하나의 도메인 주소로 접속했을 때 1번 WAS 또는 2번 WAS를 번갈아가며 접속하게 만드는 부하분산을 뜻한다. 로컬환경에서 간단히 로드밸런싱을 테스트하기 위해서는 다음 포스트를 읽어보면 좋다. ([Nginx] L4 대신 Nginx 웹서버로 로드밸런싱 하는 방법 : https://blog.naver.com/bb_/222215375652)

결국 우리가 흔히 말하는 서버 이중화 작업이란, 이러한 (1) 로드밸런싱 작업과 (2) 세션 클러스터링 작업을 포괄한 개념이다.
톰캣7 세션 클러스터링 방법은 간단하다.
1. 톰캣 폴더 내의 server.xml 파일 수정
먼저 server.xml 파일을 수정한다.
그대로 복사해서 붙여넣고 일부분만 수정하면 된다.
[AS-IS]

      <!–
      <Cluster className=”org.apache.catalina.ha.tcp.SimpleTcpCluster”/>
      –>

[TO-BE]

      <!–
      <Cluster className=”org.apache.catalina.ha.tcp.SimpleTcpCluster”/>
      –>

      <Cluster className=”org.apache.catalina.ha.tcp.SimpleTcpCluster” channelSendOptions=”8″>
     
          <Manager className=”org.apache.catalina.ha.session.DeltaManager” expireSessionsOnShutdown=”false” notifyListenersOnReplication=”true” />
     
          <Channel className=”org.apache.catalina.tribes.group.GroupChannel”>
              <Membership className=”org.apache.catalina.tribes.membership.McastService” address=”228.0.0.4″ port=”45564″ frequency=”500″ dropTime=”3000″ />
              <Receiver className=”org.apache.catalina.tribes.transport.nio.NioReceiver” address=”211.111.222.333” port=”4000” autoBind=”100″ selectorTimeout=”5000″ maxThreads=”6″ />
     
              <Sender className=”org.apache.catalina.tribes.transport.ReplicationTransmitter”>
                  <Transport className=”org.apache.catalina.tribes.transport.nio.PooledParallelSender” />
              </Sender>
              <Interceptor className=”org.apache.catalina.tribes.group.interceptors.TcpFailureDetector” />
              <Interceptor className=”org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor” />
          </Channel>
     
          <Valve className=”org.apache.catalina.ha.tcp.ReplicationValve” filter=”” />
          <Valve className=”org.apache.catalina.ha.session.JvmRouteBinderValve” />
     
          <Deployer className=”org.apache.catalina.ha.deploy.FarmWarDeployer” tempDir=”/tmp/war-temp/” deployDir=”/tmp/war-deploy/” watchDir=”/tmp/war-listen/” watchEnabled=”false” />
     
          <ClusterListener className=”org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener”/>
              <ClusterListener className=”org.apache.catalina.ha.session.ClusterSessionListener”/>
      </Cluster>

address=”211.111.222.333″ 부분에는 해당 WAS의 ip 주소를 입력하면 된다.

port=”4000″ 부분은 수신 포트로 4000~4100 사이 포트를 지정하면 된다.

동일한 PC에 톰캣 2개가 같이 있다면, 첫번째 톰캣은 4000, 두번째 톰캣은 4001… 식으로 겹치지 않게 지정하면 된다.

2. 웹 어플리케이션의 web.xml 파일 수정

web.xml 파일을 수정하는데, 주의할 점은 톰캣 내의 web.xml 파일이 아니라, 톰캣이 구동하는 웹 어플리케이션의 WEB-INF/web.xml 파일을 수정해야 한다.

예를 들어 tomcat 폴더 하위의 webapps/ROOT/WEB-INF/web.xml 파일을 말한다.

[AS-IS]

(중략)

</web-app>

[TO-BE]

(중략)

    <distributable/>

</web-app>

web-app 태그 안에 <distributable/> 를 넣어주면 된다.

3. 테스트 페이지 작성

테스트 페이지 작성은 선택사항이다. 여기서는 jsp 파일로 작성해보았다.

톰캣을 2개라고 가정하고 2개를 만든다.

(1) 톰캣 1의 test.jsp 파일

<%

    // 세션어트리뷰트에 값 세팅하기

    session.setAttribute(“tomcat1“, “Hello World”);

    out.println(“TOMCAT1“);
    out.println(“<br>”);

    out.println(“SESSION ID : ” + session.getId());
    out.println(“<br>”);
 

    // 다른 서버에서 세팅한 어트리뷰트를 꺼내보기

    String result = String.valueOf(session.getAttribute(“tomcat2“));
    out.println(“result : ” + result);
%>

(2) 톰캣 2의 test.jsp 파일

<%

    // 세션어트리뷰트에 값 세팅하기

    session.setAttribute(“tomcat2“, “Hello World”);

    out.println(“TOMCAT2“);
    out.println(“<br>”);

    out.println(“SESSION ID : ” + session.getId());
    out.println(“<br>”);

    // 다른 서버에서 세팅한 어트리뷰트를 꺼내보기
    String result = String.valueOf(session.getAttribute(“tomcat1“));
    out.println(“result : ” + result);
%>

위 jsp 페이지를 호출했을 때 처음에는 result 값이 null 이 나오지만, 몇 번 새로고침하다보면 Hello World 라는 값이 나와야 한다.

그리고 몇 번을 새로고침하더라도 세션 아이디가 바뀌지 않고 동일하게 유지되어야 한다.

4. 문제해결

기동 시 catalina.out 로그에 org.apache.catalina.ha.session.DeltaManager startInternal 이라는 문자열이 찍히는지 확인하자. 찍히지 않으면 세션 클러스터링이 적용되지 않은 것이다.

(1) web.xml 파일 안에 <distributable/> 가 있는지 확인하자. 다시 강조하지만 web.xml 파일은 톰캣 내의 web.xml 파일이 아니라, 톰캣이 구동하는 웹 어플리케이션의 WEB-INF/web.xml 파일을 수정해야 한다.

(2) web-app 태그의 version 어트리뷰트의 값이 3.0 이상인지 확인해보자. 만약 그렇지 않다면 다음 포스트를 참고하자. ([TOMCAT] 톰캣 세션 클러스터링 안되는 문제 : https://blog.naver.com/bb_/221541869532)

참고사이트 2 : https://idchowto.com/?p=53319

[Nginx] L4 대신 Nginx 웹서버로 로드밸런싱 하는 방법

[Nginx] L4 대신 Nginx 웹서버로 로드밸런싱 하는 방법

로드밸런싱이란 부하분산을 뜻한다.

큰 작업을 여러 대의 장비에게 나눠줘서 부하를 낮추는 것이 로드밸런싱이다.

웹서비스에서는 여러 대의 와스(WAS)를 한 개의 주소로 바라보게 만들 수 있다.

예를 들어서 8080, 8090 포트를 각각 사용하는 2개의 톰캣(tomcat)이 있을 때, 특정 주소(특정 도메인 80포트)에 접속하면 2개의 톰캣으로 나눠보내주는 것이다.

웹서비스에서 로드밸런싱을 위해서는 L4 스위치 장비라는 것을 이용한다.

L4 장비가 없으면 리눅스에서는 LVS(Linux Virtual Server) 라는 프로그램을 통해서도 로드밸런싱을 할 수 있다고 한다.

여기서는 L4 장비 대신 Nginx 라는 웹서버 프로그램으로 로드밸런싱을 하는 방법을 소개한다.

이 글은 윈도우 환경 기준이다. 참고로 리눅스에서도 Nginx 설치가 가능하다.

1. Nginx 다운로드

먼저 Nginx 프로그램을 다운로드해야 한다. http://nginx.org/en/download.html 에 접속한다.

안정적인 Stable version 을 다운받는다.

nginx/Windows-1.18.0 을 클릭해서 다운로드한다.

2. nginx 설치

원하는 위치에 압축을 해제하면 설치 끝이다.

예를 들면 C:\nginx-1.18.0 위치에 압축을 해제하면 된다.

3. 설정파일 수정

conf 폴더 내의 nginx.conf 파일을 수정한다.

예를 들어 C:\nginx-1.18.0\conf\nginx.conf 파일을 수정하면 된다.

(1) 업스트림 정보 입력

[AS-IS]

#gzip  on;

server {

[TO-BE] (라운드로빈 방식)

#gzip  on;

upstream myserver {
    server localhost:8080;
    server localhost:8090;
}

server {

바라보기를 원하는 서버 주소들을 입력해주면 된다.

와스(WAS)는 2대이고 각각 로컬호스트의 8080 포트, 8090 포트라고 가정한다.

위 예시는 localhost로 도메인이 같지만, 도메인이 다른 경우도 상관없다.

위와 같이 규칙을 아무것도 적지 않는다면 라운드로빈 방식으로 동작한다.

라운드로빈이란 쉽게 생각해서 균등하게 돌아가면서 방문한다고 이해하면 된다.

그런데 만약 처음 접속한 서버에 계속 머물러있게 하고 싶다면 해시 방식을 사용하면 된다.

다음과 같이 쓰면 된다.

[TO-BE] (해시 방식)

#gzip  on;

upstream myserver {

    ip_hash;


    server localhost:8080;
    server localhost:8090;
}

server {

(2) 프록시 패스 입력

[AS-IS]

location / {
    root   html;
    index  index.html index.htm;
}

[TO-BE]

location / {
    #root   html;
    #index  index.html index.htm;
    proxy_pass
http://myserver;
}

이어서 프록시 패스 값을 입력한다.

proxy_pass http://[업스트림 이름]을 쓰면 해당 업스트림 정보를 토대로 바라보게 된다.

4. Nginx 기동 후 테스트

Nginx 기동은 cmd 로 들어가서 nginx.exe 명령어를 입력하면 된다.

예를 들면 아래와 같다.

(1) Nginx 기동

cd C:\nginx-1.18.0

nginx.exe

(2)  Nginx 중지

cd C:\nginx-1.18.0

nginx.exe -s stop

Nginx 기동 후 http://localhost:80/ 으로 접속해보면 된다.

한 번은 localhost:8080 주소의 페이지가 보였다가, 또 한 번은 localhost:8090 주소의 페이지가 보이면 성공이다.

참고사이트 : https://kamang-it.tistory.com/entry/WebServernginxnginx%EB%A1%9C-%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1-%ED%95%98%EA%B8%B0

[Eclipse] 이클립스 웹 프로젝트 레진(resin) 설정 방법

[Eclipse] 이클립스 웹 프로젝트 레진(resin) 설정 방법

이클립트 웹 프로젝트를 톰캣(tomcat)이 아닌 레진(resin)으로 기동하는 방법이다.

참고로 이클립스 2020-12 버전, JDK 1.6 버전, resin 3.0.23 버전을 사용했다.

이클립스 버전과 JDK 버전은 크게 상관이 없을 것 같고, resin은 가급적 같은 버전을 사용하는 것을 권장한다.

1. 이클립스 상단 메뉴의 [Run] – [Run Configurations…] 메뉴를 클릭한다.

2. Run Configurations 윈도우가 뜨면 좌측의 Java Application 항목을 마우스 우클릭, [New] 버튼을 클릭해서 새로운 설정을 생성한다.

3. Main 탭 설정

Name 항목 : Resin 입력한다.

Project 항목 : [Browse] 버튼을 이용해서 프로젝트를 선택한다.

Main Class 항목 : com.caucho.server.resin.Resin 이라고 입력한다.

Include system libraries when searching for a main class 체크박스 : 체크 처리한다.

Include inherited mains when searching for a main class 체크박스 : 체크 처리한다.

4. Arguments 탭 설정

Program arguments 항목 : -conf “C:\프로젝트경로\폴더명\resin.conf” 를 입력한다.

ex) -conf “C:\test\workspaces\ProjectName\FolderName\resin.conf”

VM arguments 항목
-Djava.util.logging.manager=com.caucho.log.LogManagerImpl
-Djavax.management.builder.initial=com.caucho.jmx.MBeanServerBuilderImpl
-Dresin.home=”C:\resin-3.0.23″


 

갖고 있는 resin.conf 파일이 없다면 다음과 같이 생성한다.

javac compiler 부분과 document-directory 부분을 알맞게 고치면 된다.

<!–

  – Resin 3.0 configuration file.

–>

<resin xmlns=http://caucho.com/ns/resin

       xmlns:resin=http://caucho.com/ns/resin/core>

  <!–

    – Logging configuration for the JDK logging API.

  –>

  <log name= level=‘info’ path=‘stdout:’ timestamp=‘[%H:%M:%S.%s] ‘/>

  <log name=‘com.caucho.java’ level=‘config’ path=‘stdout:’

       timestamp=‘[%H:%M:%S.%s] ‘/>

  <log name=‘com.caucho.loader’ level=‘config’ path=‘stdout:’

       timestamp=‘[%H:%M:%S.%s] ‘/>

 

  <!–

     – For production sites, change dependency-check-interval to something

     – like 600s, so it only checks for updates every 10 minutes.

  –>

  <dependency-check-interval>2s</dependency-check-interval>

 

  <!–

     – You can change the compiler to “javac” or jikes.

     – The default is “internal” only because it’s the most

     – likely to be available.

  –>

  <javac compiler=C:\java\jdk1.6.0_45\bin\javac.exe args=“”/>

 

  <!– Security providers.

     – <security-provider>

         com.sun.net.ssl.internal.ssl.Provider

     – </security-provider>

  –>

 

  <!–

     – If starting bin/resin as root on Unix, specify the user name

     – and group name for the web server user.

    

     – <user-name>resin</user-name>

     – <group-name>resin</group-name>

  –>

 

  <!–

     – Configures threads shared among all HTTP and SRUN ports.

  –>

  <thread-pool>

    <!– Maximum number of threads. –>

    <thread-max>128</thread-max>

 

    <!– Minimum number of spare connection threads. –>

    <spare-thread-min>25</spare-thread-min>

  </thread-pool>

 

  <!–

     – Configures the minimum free memory allowed before Resin

     – will force a restart.

  –>

  <min-free-memory>1M</min-free-memory>

 

  <server>

    <!– adds all .jar files under the resin/lib directory –>

    <class-loader>

      <tree-loader path=“$resin-home/lib”/>

    </class-loader>

 

    <!– Configures the keepalive –>

    <keepalive-max>500</keepalive-max>

    <keepalive-timeout>120s</keepalive-timeout>

 

    <!– The http port –>

    <http server-id=“” host=“*” port=“80”/>

 

    <!–

       – SSL port configuration:

      

       – <http port=”8443″>

          <openssl>

            <certificate-file>keys/gryffindor.crt</certificate-file>

            <certificate-key-file>keys/gryffindor.key</certificate-key-file>

            <password>test123</password>

          </openssl>

       </http>

    –>

 

    <!–

       – The local cluster, used for load balancing and distributed

       – backup.

    –>

    <cluster>

      <srun server-id=“” host=“127.0.0.1” port=“6803” index=“1”/>

    </cluster>

 

    <!–

       – Enables/disables exceptions when the browser closes a connection.

    –>

    <ignore-client-disconnect>true</ignore-client-disconnect>

 

    <!–

       – Enables the cache

    –>

    <!–

    <cache path=”cache” memory-size=”10M”/>

    –>

 

    <!–

       – Enables periodic checking of the server status.

      

       – With JDK 1.5, this will ask the JDK to check for deadlocks.

       – All servers can add <url>s to be checked.

    –>

    <!–

    <ping>

      <url></url>”>http://localhost:8080/test-ping.jsp</url>

    </ping>

    –>

 

    <!–

       – Defaults applied to each web-app.

    –>

    <web-app-default>

      <!–

      – Sets timeout values for cacheable pages, e.g. static pages.

      –>

      <cache-mapping url-pattern=“/” expires=“5s”/>

      <cache-mapping url-pattern=“*.gif” expires=“60s”/>

      <cache-mapping url-pattern=“*.jpg” expires=“60s”/>

 

      <!–

        Servlet to use for directory display.

      –>

      <servlet servlet-name=“directory”

              servlet-class=“com.caucho.servlets.DirectoryServlet”/>

    </web-app-default>

 

    <!–

       – Sample database pool configuration

      

       – The JDBC name is java:comp/env/jdbc/test

      

         <database>

           <jndi-name>jdbc/mysql</jndi-name>

           <driver type=”org.gjt.mm.mysql.Driver”>

             <url>jdbc:mysql://localhost:3306/test</url>

             <user></user>

             <password></password>

            </driver>

            <prepared-statement-cache-size>8</prepared-statement-cache-size>

            <max-connections>20</max-connections>

            <max-idle-time>30s</max-idle-time>

          </database>

    –>

 

    <!–

       – Default host configuration applied to all virtual hosts.

    –>

    <host-default>

      <class-loader>

        <compiling-loader path=‘webapps/WEB-INF/classes’/>

        <library-loader path=‘webapps/WEB-INF/lib’/>

      </class-loader>

 

      <!–

         – With another web server, like Apache, this can be commented out

         – because the web server will log this information.

      –>

      <access-log path=‘logs/access.log’

            format=‘%h %l %u %t “%r” %s %b “%{Referer}i” “%{User-Agent}i”‘

            rollover-period=‘1W’/>

 

      <!– creates the webapps directory for .war expansion –>

      <web-app-deploy path=‘webapps’/>

 

      <!– creates the deploy directory for .ear expansion –>

      <ear-deploy path=‘deploy’>

        <ear-default>

          <!– Configure this for the ejb server

            

             – <ejb-server>

                <config-directory>WEB-INF</config-directory>

                <data-source>jdbc/test</data-source>

             – </ejb-server>

          –>

        </ear-default>

      </ear-deploy>

 

      <!– creates the deploy directory for .rar expansion –>

      <resource-deploy path=‘deploy’/>

 

      <!– creates a second deploy directory for .war expansion –>

      <web-app-deploy path=‘deploy’/>

    </host-default>

 

    <!– includes the web-app-default for default web-app behavior –>

    <resin:import path=“${resinHome}/conf/app-default.xml”/>

 

    <!– configures the default host, matching any host name –>

    <host id=>

      <document-directory>C:\test\workspaces\ProjectName\FolderName\webapp</document-directory>

 

      <!– configures the root web-app –>

      <web-app id=>

      <document-directory>C:\test\workspaces\ProjectName\FolderName\webapp</document-directory>

        <class-loader/>

        <servlet-mapping url-pattern=“/servlet/*” servlet-name=“invoker”/>

       

          <!– 최초 기동시 실행할 서블릿 –>

          <!-

          <servlet servlet-name=“starton” servlet-class=“com.test.servlet.HomeServlet”>

            <load-on-startup />

          </servlet>

                  –>

      </web-app>

    </host>

  </server>

</resin>

5. JRE 탭 설정

특별한 설정은 없다. 여기서는 JDK 1.6을 사용했지만 다른 버전이어도 상관없다.

 

6. Classpath 탭 설정

우측의 [Advanced] 버튼을 클릭해서 RESIN 이라는 이름의 User Library 를 추가해주자.

레진 관련 라이브러리를 한꺼번에 담기 위한 방법이다.

 

우측의 [Advanced] 버튼을 클릭 – [Add Library] 라디오 버튼 선택하고 [OK] 버튼 클릭 – [User Library] 항목 선택 – [User Libraries…] 버튼 클릭

 

우측의 [New…] 버튼을 이용해서 RESIN 라이브러리를 생성하고, [Add External JARs…] 버튼을 통해 프로젝트 외부에 있는 레진 관련 라이브러리들을 모두 추가하자. 예를 들면 C:\resin-3.0.23\lib 폴더 내의 모든 jar 파일을 추가하면 된다.

7. Source 탭 설정

특별한 설정이 없다.

 

8. Environment 탭 설정

특별한 설정이 없다.

 

9. Common 탭 설정

특별한 설정은 없고 필요하다면 Encoding 값을 UTF-8로 지정해주자.


여기까지가 이클립스에서 웹 프로젝트를 레진(resin)으로 기동하기 위한 설정 방법이다.

[Eclipse] Java11 (JDK11) 설치 이후 이클립스 실행 시 PostConstruct, PreDestroy 오류

[Eclipse] Java11 (JDK11) 설치 이후 이클립스 실행 시 PostConstruct, PreDestroy 오류

Java11 (JDK 11)을 설치하고 기존에 잘 동작하던 이클립스를 실행했더니 “An error has occurred. See the log file C:\eclipse\workspaces\testproject\.metadata\.log.” 메시지가 표시됐다.

해당 로그 파일을 메모장으로 열어보니 아래처럼 오류 2개가 기록되어 있었다.

(1) org.eclipse.e4.core.di.InjectionException: java.lang.NoClassDefFoundError: javax/annotation/PostConstruct

(2) java.lang.NoClassDefFoundError: javax/annotation/PreDestroy

실행한 이클립스 실행파일인 eclipse.exe 와 같은 폴더에 존재하는 eclipse.ini 파일에 아래와 같이 내용을 수정했다.

(1) JDK8을 사용하도록 수정

-vm
C:/Java/jdk1.8.0_112/bin/javaw.exe

(2) -vmargs 아래에 –add-modules=ALL-SYSTEM 을 추가

-vmargs
–add-modules=ALL-SYSTEM

이후 이클립스가 정상적으로 기동되는 것을 확인했다.

참고사이트 : https://dorbae.github.io/tool/eclipse/Tool-Eclipse-SetupVM/

[Windows] 하드디스크 드라이브의 노란색 느낌표 및 자물쇠 아이콘 제거하는 방법

[Windows] 하드디스크 드라이브의 노란색 느낌표 및 자물쇠 아이콘 제거하는 방법

윈도우 내 PC에 들어갔을 때 하드디스크 드라이브(C 드라이브, D 드라이브) 에 아래 그림처럼 노란색 느낌표와 자물쇠 아이콘이 나타난 경우 제거하는 방법.

 

1. 명령 프롬프트(cmd)를 관리자 권한으로 실행

좌측 하단 [윈도우 아이콘] 클릭 –  [cmd] 입력 – [명령 프롬프트] 마우스 우클릭 – [관리자 권한으로 실행] 클릭한다.

2. 명령 프롬프트(cmd) 창에 manage-bde -off [드라이브명] 입력

C 드라이브일 경우, manage-bde -off c: 입력한다.

D 드라이브일 경우, manage-bde -off d: 입력한다.

[암호 해독이 지금 진행 중입니다.] 텍스트가 나올 때까지 기다린다.

 

3. 해결되었는지 확인

다시 내 PC로 들어가보면 C 드라이브 또는 D 드라이브의 노란색 느낌표 및 자물쇠 아이콘이 없어져 있다.


참고사이트 : https://answers.microsoft.com/ko-kr/windows/forum/all/d-%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B8%8C%EC%9D%98/139a4588-7540-4094-a2e5-fa75651935ce

[JAVA] JAVA (JNA) KEYBD_EVENT 구현하기

[JAVA] JAVA (JNA) KEYBD_EVENT 구현하기

JAVA 에서 JNA 를 통해서 KEYBD_EVENT 구현하는 방법.

자바 키입력은 Robot 클래스를 사용할 수도 있지만, JNA를 통하면 윈도우 user32 라이브러리의 KEYBD_EVENT 를 구현해서 쓸 수 있다.

Hwnd 를 다룰 일이 은근히 많아서 깃헙에 Wrapper 형태로 유지보수할 생각이다.

아래는 자바에서 특정한 윈도우 핸들(hWnd)을 포커싱하고 키입력하는 기능을 제공하는 코드다.

0. 필요한 라이브러리

jna-4.5.0.jar, jna-platform-4.5.0.jar

1. MainClass.java

현재 실행 중인 윈도우 핸들(ex : 메모장)을 열어서 Hello World 라는 문자열을 입력하도록 만들어봤다.

package com.thkmon.hwndtest;

import java.awt.event.KeyEvent;

import com.sun.jna.platform.win32.WinDef.HWND;

public class MainClass {

    public static void main(String[] args) {
        
        System.out.println(“시작”);
        
        // 현재 실행 중인 메모장 윈도우 핸들을 열어서 Hello World 를 입력하기
        try {
            String hwndText = “메모장”;
            
            HwndFinder hwndFinder = new HwndFinder();
            HWND hwnd = hwndFinder.getSingleHwnd(hwndText);
            
            if (hwnd != null && HwndUtil.setFocusHandle(hwnd)) {
                HwndUtil.inputKey(hwnd, KeyEvent.VK_H); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_E); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_O); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_SPACE); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_W); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_O); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_R); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100);
                HwndUtil.inputKey(hwnd, KeyEvent.VK_D); sleep(100);
                
                sleep(100);

                
            } else {
                System.out.println(“윈도우 핸들(“ + hwndText + “)을 찾을 수 없습니다.”);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        System.out.println(“끝”);
    }
    
    
    private static void sleep(int i) {
        try {
            if (i > 0) {
                Thread.sleep(i);
            }
        } catch (InterruptedException e) {
        } catch (Exception e) {}
    }
}

이때 몇몇 키는 KeyEvent 클래스에 정의되어있지 않다.

대표적으로 엔터키의 값이 정의되어 있지 않은데, 이런 키 값들은 인터넷에서 검색해서 값을 찾아 사용하면 된다.

예를 들어 엔터키 입력이 필요하면 아래와 같이 작성하면 된다.


private static final int VK_RETURN = 0x0D;


// 중략

HwndUtil.inputKey(hwnd, VK_RETURN);

sleep(100);

2. HwndFinder.java

윈도우 핸들(Hwnd)을 찾기 위한 클래스이다. getSingleHwnd 메서드는 1개의 Hwnd만을 리턴한다.

Hwnd 를 n개 리턴받고 싶다면 아래 코드를 변경해서 ArrayList<Hwnd> 에 담으면 되겠다.

package com.thkmon.hwndtest;

import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinDef.RECT;
import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;

public class HwndFinder {
    
    private HWND handle = null;
    
    
    public HWND getSingleHwnd(String nameOrClass) throws NullPointerException, Exception {
        handle = null;
        
        setSingleHwnd(nameOrClass, true);
        if (handle != null) {
            return handle;
        }
        
        setSingleHwnd(nameOrClass, false);
        if (handle != null) {
            return handle;
        }
        
        return null;
    }
    
    
    private void setSingleHwnd(final String nameOrClass, final boolean bEquals) throws NullPointerException, Exception {
        
        try {
            User32.INSTANCE.EnumWindows(new WNDENUMPROC() {
                public boolean callback(HWND hWnd, Pointer arg1) {

                    // 이미 찾았으면 스킵
                    if (handle != null) {
                        return true;
                    }

                    char[] windowText = new char[512];
                    User32.INSTANCE.GetWindowText(hWnd, windowText, 512);
                    String wText = Native.toString(windowText);
                    
                    RECT rectangle = new RECT();
                    User32.INSTANCE.GetWindowRect(hWnd, rectangle);
                    
                    // 숨겨져 있는 창은 찾지 않는다.
                    // 단, 최소화 되어있는 창은 찾는다. rectangle.left값이 -32000일 경우 최소화되어 있는 창이다.
                    // if (wText.isEmpty() || !(User32.INSTANCE.IsWindowVisible(hWnd) && rectangle.left > -32000)) {
                    if (wText.isEmpty() || !(User32.INSTANCE.IsWindowVisible(hWnd))) {
                        return true;
                    }

                    // 핸들의 클래스 네임 얻기
                    char[] c = new char[512];
                    User32.INSTANCE.GetClassName(hWnd, c, 512);
                    String clsName = String.valueOf(c).trim();

                    // int count = 0;
                    // System.out.println(
                    //         // “hwnd:”+hWnd+“,”+
                    //         “번호:” + (++count) + “,텍스트:” + wText + “,” + “위치:(“ + rectangle.left + “,” + rectangle.top
                    //                 + “)~(“ + rectangle.right + “,” + rectangle.bottom + “),” + “클래스네임:” + clsName);
                    
                    if (bEquals) {
                        if (clsName != null && clsName.equals(nameOrClass)) {
                            handle = hWnd;
                        }
                        
                        if (wText != null && wText.equals(nameOrClass)) {
                            handle = hWnd;
                        }
                    } else {
                        if (clsName != null && clsName.indexOf(nameOrClass) > -1) {
                            handle = hWnd;
                        }
                        
                        if (wText != null && wText.indexOf(nameOrClass) > -1) {
                            handle = hWnd;
                        }
                    }

                    return true;
                }
            }, null);

        } catch (Exception e) {
            throw e;
        }
    }
}

3. HwndUtil.java

Hwnd 에 관련해서 필요한 메서드들을 한 군데 모은 클래스다.

Hwnd 윈도우의 텍스트 가져오기, Hwnd 윈도우의 클래스명 가져오기, Hwnd 윈도우의 프로세스 아이디(pid)가져오기, Hwnd 윈도우 포커스, Hwnd 윈도우 최소화 여부 판단, Hwnd 윈도우 닫기, 2개의 Hwnd 윈도우 일치하는지 판단, 특정키를 입력하는 keybd_event 메서드 등의 기능을 제공한다.

package com.thkmon.hwndtest;

import com.sun.jna.Native;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinDef.RECT;
import com.sun.jna.platform.win32.WinUser;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;

public class HwndUtil {

    
    /**
     * GetForegroundWindow 메서드, keybd_event 메서드 구현을 위한 인터페이스 정의
     *
     * @author bbmon
     *
     */

    public interface CustomUser32 extends StdCallLibrary {
        CustomUser32 INSTANCE = (CustomUser32) Native.loadLibrary(“user32”, CustomUser32.class);
        HWND GetForegroundWindow();
        void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
    }
    
    
    /**
     * 핸들의 텍스트 가져오기
     *
     * @param hwnd
     * @return
     */

    public static String getHandleText(HWND hwnd) {
        char[] windowText = new char[512];
        User32.INSTANCE.GetWindowText(hwnd, windowText, 512);
        String wText = Native.toString(windowText);
        return wText;
    }

    
    /**
     * 핸들의 클래스명 가져오기
     * @param hwnd
     * @return
     */

    public static String getHandleClassName(HWND hwnd) {
        char[] c = new char[512];
        User32.INSTANCE.GetClassName(hwnd, c, 512);
        String clsName = String.valueOf(c).trim();
        return clsName;
    }
    
    
    /**
     * 핸들의 pid 가져오기
     * @param hwnd
     * @return
     */

    public static int getHandlePid(HWND hwnd) {
        IntByReference pidByRef = new IntByReference(0);
        User32.INSTANCE.GetWindowThreadProcessId(hwnd, pidByRef);
        int pid = pidByRef.getValue();
        return pid;
    }

    
    /**
     * 핸들 포커스
     *
     * @param hwnd
     */

    public static boolean setFocusHandle(HWND hwnd) {
        // 최소화 되어있을 경우 복원
        if (isMinimizedHandle(hwnd)) {
            User32.INSTANCE.ShowWindow(hwnd, 9);
        }
        
        User32.INSTANCE.SetForegroundWindow(hwnd);
        
        try {
            Thread.sleep(100);
        } catch (Exception e) {}
        
        // 포커스 되었는지 확인해야 한다.
        HWND foregroundHwnd = CustomUser32.INSTANCE.GetForegroundWindow();
        boolean bFocused = checkHandlesAreSame(hwnd, foregroundHwnd);
        return bFocused;
    }
    
    
    /**
     * 최소화되어 있는 창인지 검사한다. rectangle.left값이 -32000일 경우 최소화되어 있는 창이다.
     *
     * @param hwnd
     */

    public static boolean isMinimizedHandle(HWND hwnd) {
        if (hwnd == null) {
            return false;
        }
        
        RECT rectangle = new RECT();
        User32.INSTANCE.GetWindowRect(hwnd, rectangle);
        if (rectangle.left <= -32000) {
            return true;
        }
        
        return false;
    }

    
    /**
     * 창을 강제로 닫는다.
     *
     * @param hwnd
     */

    public static void closeHwnd(HWND hwnd) {
        User32.INSTANCE.PostMessage(hwnd, WinUser.WM_CLOSE, null, null);
    }
    
    
    /**
     * 두 개의 핸들 객체가 일치하는지 확인한다.
     *
     * @param hwnd1
     * @param hwnd2
     * @return
     */

    public static boolean checkHandlesAreSame(HWND hwnd1, HWND hwnd2) {
        if (hwnd1 == null) {
            return false;
        }
        
        if (hwnd2 == null) {
            return false;
        }
        
        // pid가 같아도 핸들은 다를 수 있다. 예를 들어 엑셀 시트 창과 엑셀의 오류 메시지 창은 같은 pid를 갖지만 핸들은 다르다.
        int pid1 = HwndUtil.getHandlePid(hwnd1);
        int pid2 = HwndUtil.getHandlePid(hwnd2);
        if (pid1 != pid2) {
            return false;
        }
        
        String className1 = HwndUtil.getHandleClassName(hwnd1);
        String className2 = HwndUtil.getHandleClassName(hwnd2);
        if (className1 == null || className2 == null || !className1.equals(className2)) {
            return false;
        }
        
        String text1 = HwndUtil.getHandleText(hwnd1);
        String text2 = HwndUtil.getHandleText(hwnd2);
        if (text1 == null || text2 == null || !text1.equals(text2)) {
            return false;
        }
        
        return true;
    }
    
    
    /**
     * 특정 키를 누른다.
     *
     * @param customUser32
     * @param hwnd
     * @param vkCode
     * @throws Exception
     */

    public static void inputKey(HWND hwnd, int vkCode) throws Exception {
        CustomUser32.INSTANCE.keybd_event((byte) vkCode /* KeyEvent.VK_ESCAPE */, (byte) 0, 0, 0);
        CustomUser32.INSTANCE.keybd_event((byte) vkCode /* KeyEvent.VK_ESCAPE */, (byte) 0, 2 /* KEYEVENTF_KEYUP */, 0);
    }
}

4. 실행결과

현재 실행 중인 메모장 윈도우를 찾아서 자동으로 hello world 문자열을 입력한 모습이다.

참고사이트 : https://develop88.tistory.com/entry/JNA-%ED%82%A4%EB%B3%B4%EB%93%9C%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D-keybdevent

[Apache] (OS 64)지정된 네트워크 이름을 더 이상 사용할 수 없습니다. AH00341: winnt_accept: Asynchronous AcceptEx failed

[Apache] (OS 64)지정된 네트워크 이름을 더 이상 사용할 수 없습니다. AH00341: winnt_accept: Asynchronous AcceptEx failed

아파치 웹서버 2.4 에러로그에 “(OS 64)지정된 네트워크 이름을 더 이상 사용할 수 없습니다. AH00341: winnt_accept: Asynchronous AcceptEx failed” 오류가 발생한 경우.

httpd.conf 파일을 아래와 같이 수정한다.

[AS-IS]

#EnableMMAP off

#EnableSendfile off

[TO-BE]

EnableMMAP off

EnableSendfile off

AcceptFilter http none

P.S. 아파치 웹서버를 http로 접속하면 속도가 빠른데 https 는 속도가 느린 현상이 있었다. 위 작업을 하고 현상이 개선되었다.

참고사이트 : https://mungpack.tistory.com/149

[Android] 안드로이드 웹뷰(WebView) 확대 축소 버튼 없애기 (줌 컨트롤 숨김처리)

[Android] 안드로이드 웹뷰(WebView) 확대 축소 버튼 없애기 (줌 컨트롤 숨김처리)

안드로이드 모바일 앱의 웹뷰(WebView) 에서 화면 확대/축소를 가능하게 하려면 해당 htm, html 페이지의 소스코드를 일부 수정하고 안드로이드 앱의 소스코드도 수정해야 한다.

1. htm, html 소스코드 수정

[AS-IS]

<html>
    <head>
      <meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8″>
      <meta name=”viewport” content=”viewport-fit=cover, minimal-ui, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no” />

    </head>

</html>

[TO-BE]

<html>
    <head>
      <meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8″>
      <meta name=”viewport” content=”viewport-fit=cover, minimal-ui, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0, user-scalable=yes” />

    </head>

</html>

2. 안드로이드 앱의 소스코드 수정

웹뷰 객체의 변수명이 mWebView 일 경우 아래처럼 코딩한다.

// 모바일 확대/축소 가능하도록 수정

mWebView.getSettings().setBuiltInZoomControls(true);​

// 모바일 확대/축소 버튼 없애기 (줌 컨트롤 숨김처리)

mWebView.getSettings().setDisplayZoomControls(false);

참고) 모바일 확대/축소 버튼(줌 컨트롤) 이미지

참고사이트 : https://www.masterqna.com/android/58545/%EC%9B%B9%EB%B7%B0-%ED%99%95%EB%8C%80-%EC%B6%95%EC%86%8C%EC%97%90-%ED%95%98%EB%8B%A8%EC%97%90-%ED%99%95%EB%8C%80-%EC%B6%95%EC%86%8C%EB%B2%84%ED%8A%BC%EC%95%84%EC%9D%B4%EC%BD%98-%EC%97%86%EC%95%A0%EB%8A%94%EB%B2%95-%EC%9E%88%EB%82%98%EC%9A%94

[Android] Error while Launching activity 오류 해결

[Android] Error while Launching activity 오류 해결

Refactor 기능을 이용해서 안드로이드 프로젝트의 패키지명을 변경했더니 Run 또는 Debug 수행 시 아래처럼 Error while Launching activity 오류가 발생하며 앱이 실행되지 않았다.

$ adb shell am start -n “패키지명.하위패키지명/패키지명.하위패키지명.메인클래스명” -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Error while executing: am start -n “패키지명.하위패키지명/패키지명.하위패키지명.메인클래스명” -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=패키지명.하위패키지명/패키지명.하위패키지명.메인클래스명 launchParam=MultiScreenLaunchParams { mDisplayId=0 mBaseDisplayId=0 mFlags=0 } }
Error type 3
Error: Activity class {패키지명.하위패키지명/패키지명.하위패키지명.메인클래스명} does not exist.

Error while Launching activity

해결책

1. build.gradle 파일과 AndroidManifest.xml 파일에 기입된 패키지명이 잘못되었는지 확인

2. AndroidManifest.xml 파일 또는 build.gradle 파일에 있는 versionName 값을 수정해가며 빌드하기

ex) 기존 versionName “1.0”인 경우, “1.0”을 “1.1”로 수정하고 다시 빌드, 이후 “1.1”을 다시 “1.0”으로 되돌려놓고 다시 빌드

3. 안드로이드 스튜디오 상단 메뉴의 [Build] – [Rebuild Project] 메뉴와, [Build] – [Clean Project] 메뉴를 클릭하여 다시 빌드한 이후 재시도

4. 기존에 설치되어 있는 앱을 삭제하고 재시도

5. 안드로이드 스튜디오를 종료하고 다시 실행하여 재시도

참고사이트 : https://blog.naver.com/myohyun/220972512528

[Python] ModuleNotFoundError: No module named ‘selenium’

[Python] ModuleNotFoundError: No module named ‘selenium’

File “파일명.py”, line 1, in <module>

import selenium

ModuleNotFoundError: No module named ‘selenium’

위 오류가 발생할 경우, 터미널(윈도우라면 cmd)에서 아래 코드로 selenium 을 설치한다.

pip install selenium

참고사이트 : https://www.inflearn.com/questions/37494

[Xcode] Linker command failed with exit code 1 – duplicate symbol

[Xcode] Linker command failed with exit code 1 – duplicate symbol

Xcode 에서 빌드할 때 Linker command failed with exit code 1 – duplicate symbol 오류가 발생하는 경우.

프로젝트를 열 때 xcodeproj 확장자 파일을 더블클릭해서 열지 말고, xcworkspace 확장자 파일을 더블클릭해서 열어야 한다. 현재 프로젝트 윈도우를 닫고 xcworkspace 확장자로 프로젝트를 열면 해결된다.

참고사이트 : https://stackoverflow.com/questions/40365354/linker-command-failed-with-exit-code-1-duplicate-symbol-tmrbbp/55219349

[Apache] Apache 2.4.4 with openssl-1.0.1e 아파치 웹서버 다운로드

[Apache] Apache 2.4.4 with openssl-1.0.1e 아파치 웹서버 다운로드

apache_2.4.4-x64-openssl-1.0.1e.msi (64비트용)

다운로드 : http://www.mediafire.com/file/utf9k7qf64equt6/apache_2.4.4-x64-openssl-1.0.1e.msi

apache_2.4.4-x86-openssl-1.0.1e.msi (32비트용)

다운로드 : http://www.mediafire.com/file/1l78i2tve4vsowy/apache_2.4.4-x86-openssl-1.0.1e.msi

Apple Business Manager 란 무엇인가

Apple Business Manager 란 무엇인가

애플 엔터프라이즈 라이선스 발급이 거부됐을 경우, 애플 측은 ‘Apple Business Manager’ 를 사용하면 된다고 안내한다.

그러나 Apple Business Manager 로 독점 앱을 배포하고 관리하는 것이 굉장히 까다롭다는 생각이 들고 구체적인 실행방안이 잘 파악되지 않아서 관련 문서들을 일단 정리해둔다.

Apple Business Manager

모바일 기기 관리 솔루션을 통해 직원에게 독점 앱을 배포하면 기기 등록, 콘텐츠 배포 및 관리 권한 위임이 간편해집니다. 또한 개발자는 Apple Business Manager를 사용하여 코드를 유지하고 지적 재산권을 보유하면서 사용자화 앱을 특정 회사에 비공개로 제공할 수 있습니다. 회사는 Apple Business Manager 계정을 생성하여 앱을 보고 다운로드할 수 있습니다.

결론을 요약하면, Apple Business Manager(ABM)에서 앱을 배포하려면 MDM 서버가 있어야 한다. Apple Business Manager는 MDM 없이 작동할 수 없다. 

조직 구성원이 200명 이상일 경우 애플 엔터프라이즈 라이선스 발급을 신청하도록 하고, 그렇지 않으면 MDM 서버를 통해서 Apple Business Manager 로 내부용 앱을 배포할 수 있다고 한다. 조직 내부용 앱에 대해서 엔터프라이즈 계정 발급이 거부되었고, MDM 서버 구축도 여의치 않다면 앱이 아닌 웹 서비스 형태(아이폰 사파리에서 접속)를 고려해봐야 한다.

■ 맞춤형 앱 배포하기

https://developer.apple.com/kr/custom-apps/

■ iPhone 및 iPad용 배포 참조

https://support.apple.com/ko-kr/guide/deployment-reference-ios/welcome/web

■ Apple Business Manager 사용 설명서

https://support.apple.com/ko-kr/guide/apple-business-manager/welcome/web#/apdd344cdd9d

■ 동영상 (제목 : Custom app distribution with Apple Business Manager)

위 동영상에 나오는 이미지


(제일 오른쪽의 Custom 이 Apple Business Manager 에 해당됨)



 

■ 지식인에 올라온 질문글

제목 : 애플 비즈니스 매니저 (Apple Business Manager) 질문 / iOS 앱 배포

IT회사에 재직 중인 개발자입니다. 

회사에서 만든 iOS 앱을 특정 고객사에 배포해야하지만 불특정 다수의 인원이기 때문에

TestFlight, Add-hoc … 보다는 간단하고 오래 다운받게 하고 싶습니다.

처음에 고려한건 애플 기업 엔터프라이즈 계정으로 등록하여 in-house 방식으로 내부 배포하는 것 처럼

url을 통해 쉽게 배포하고 싶었으나 프로그램 등록 과정에서 100명이상인 회사여야한다는 조건이 부합되지 않아 리젝 당하였습니다.

답변으로는 애플 비즈니스 매니저를 사용하라는데 나온지 얼마안되서 그런지 정보가 없어서 접근하기가 좀 어려운 상태입니다. 

그래서 앱을 배포하는 개념으로 간편하다면 사용하고 싶은데..

애플 비즈니스 매니저에 대해 설명 및 배포방법에 대해 알려주실 수 있을까요??

기업용 개발자 등록을 하려했으나 사내에서 만든 특정 고객용 앱은 앱스토어 올릴 수 없다는 이야기도 들었습니다.

———-

보물섬 님 답변

로그인을 해야 하는 앱으로 만들어 앱스토어 올리면 로그인을 할 수 있는 특정 사람에게만 사용할 수 있도록 가능 합니다. 이렇게 해서 배포하는 회사들도 있어요.

그리고 애플 비즈니스 매니저는 사내 IT 매니져가 앱을 대량으로 구입하여 사내에 배포하는 것으로 생각 하시면 될거 같습니다. 쉽게 이야기 해서 사내에서 윈도우 정품을 볼륨 라이센스로 구입해서 직원들에게 설치 해 주잖아요? 그런거와 좀 유사한거 같습니다. 그리고 아래 글 보시면 사내 배포용 앱(커스텀 앱)을 만들어 배포도 가능한데 앱스토어와 똑같이 앱 리뷰는 받아야 합니다.

Distribution: Custom Apps are distributed privately within the Apps and Books section of Apple Business Manager. IT teams can leverage the same app distribution model as App Store apps, including device-based assignments and managed app capabilities. Developers designate organizations that are allowed to access the app and can privately distribute to specific organizations. If you’re supporting a global user base, it’s important to mark the app as globally available when submitting through App Store Connect.

(배포 : 사용자 지정 앱은 Apple Business Manager의 앱 및 책 섹션에서 비공개로 배포됩니다. IT 팀은 장치 기반 할당 및 관리되는 앱 기능을 포함하여 App Store 앱과 동일한 앱 배포 모델을 활용할 수 있습니다. 개발자는 앱에 액세스할 수 있고 특정 조직에 비공개로 배포할 수있는 조직을 지정합니다. 전 세계 사용자층을 지원하는 경우 App Store Connect를 통해 제출할 때 앱을 전 세계에서 사용할 수있는 것으로 표시하는 것이 중요합니다.)

■ 네이버카페에 올라온 질문글

(출처 : https://cafe.naver.com/mcbugi/349080)

제목 : Apple Business Manager(애플비즈니스매니져)에 대해 혹시 아시는분 계신가요? 

작성자 : 노란창문

기업용 엔터프라이즈계정 신청을 했는데~

애플에서 Apple Business Manager 사용하라고 메일이 왔네요.

잠깐 구글링해보니… 모.. mdm 서버거 어떻고, 기기관리를 하고… 얘기가 나온는데~ 도통 알수가 없네요. TT

이걸로 엔터프라이즈계정을 통한 앱배포처럼 사용자들이 앱을 설치하여 이용할 수가 있는건가요?

사용자는 불특정 다수입니다.(사용대상자는 기업내에서 사용을 할거지만, 외부사용자도 있습니다.)

혹시 Apple Business Manager 사용해본 경험이 있으신분 조언좀 부탁드리겠습니다.

엔터프라이즈계정을 대신하여 사용해도 별문제가 없는것인지…

앱배포까지의 절차는 기존방식과 큰 차이가 없는건지등도 궁금하네요. T_T

———-

앨런 : MDM 개발할 당시에 Apple Business Manager가 한국에서 오픈을 안해서, 사용 못해봤습니다. 지금은 오픈했나요? 

아무튼 MDM 방식은 과거에도 있었고, 엔터프라이즈 개발자와 별개로 동작합니다. 

MDM을 통하면 아이폰 장비자체에 제어권을 넘겨받아 앱을 강제로 설치하거나 지울수 있는 기능있습니다. 

엔터프라이즈의 다운로드형 인하우스 배포앱과는 방식이 다릅니다.

노란창문 : 답글감사합니다. 

애플상담사를 통해 알아봤는데… 말씀하신대로 앱배포를 위해 mdm서버가 필요하고… 배포관리를 애플비즈니스매니져를 통해 하는데~ 앱을 인하우스용으로 배포/관리 목적으로 사용하는거였네요. 엔터프라이즈계정 필요없이요~ 

저희는 당장 mdm서버 구축문제때문에… 걍 엔터프라이즈계정으로 할수 밖에 없을거 같습니다

■ appypie 에 올라온 포스트 번역

(출처 : https://www.appypie.com/how-to-distribute-custom-ios-apps-for-businesses)

모든 규모와 규모의 비즈니스 홍보를위한 가장 큰 수단으로 자리 잡은 모바일 앱은 단순한 기술 그 이상입니다. 모바일 앱 개발은 지평을 넓혔으며 현대 소비자 라이프 스타일의 중요한 부분으로 자리 매김했습니다. 모바일 앱 시류의 궤적이 올라가고 있습니다. 매일 수천 개의 스마트 폰 앱이 앱 내 구매, 푸시 알림, 엔터프라이즈 앱, 약속 예약, 챗봇 등과 같은 다양한 기능과 함께 앱 스토어에 게시됩니다!

오늘날 앱 시장에서 유일한 두 명의 플레이어는 Android와 iOS입니다. 그러나 품질과 일관성에 관해서는 Apple 또는 iOS가 Android보다 분명히 우위에 있습니다. Apple이 나머지 앱 스토어와 차별화되는 점은 iOS 플랫폼 용 맞춤형 앱을 배포하는 데 사용할 수있는 광범위한 프로그램입니다.

이 기사에서는 iOS 스토어에 앱을 배포하는 가능한 모든 방법에 대해 설명합니다. 따라서 기업 내에서 사용자 지정 앱을 배포하든 공용 앱을 배포하든 관계없이 iOS 앱 배포는 앱 바이너리가 장치에 전달되는 방식과 라이선스가 처리되는 방식이라는 두 가지 다른 특성에 따라 분류됩니다.

■ 올바른 옵션 분류

일반적으로 앱 배포에는 네 가지 방법이 있지만 iOS 앱 배포 요구에 적합한 선택은 한 가지 방법뿐입니다. 다음은 Apple App Store에서 앱을 배포하는 네 가지 방법입니다.

각 방법에 대해 자세히 살펴 보겠습니다.

■ App Store 배포

iOS 장치 용 사용자 지정 앱을 배포하는 가장 일반적인 방법 중 하나는 Apple App Store입니다. Apple App Store에 앱을 배포하는 것은 많은 SMB와 개인이 선택하지만,이 평판이 좋은 앱 스토어가 제공하는 모든 것에 대해 명확한 아이디어를 가진 기업은 소수에 불과합니다. 예를 들어 앱을 무료 또는 고정 가격으로 배포할 수 있으며 누구나 iOS 기기 및 Apple ID로 액세스할 수 있습니다. 또한 인앱 구매, iAD 네트워크, 유료 구독 등 다양한 옵션을 통해 앱으로 수익을 창출할 수도 있습니다.

하지만 Apple App Store는 사용자 친화적이고 사용하기 쉽지만 앱 승인 프로세스를 따르는 것은 전 세계의 많은 앱 개발자에게 항상 어려운 작업이었습니다. 초기 검토를 위해 앱을 제출 한 후 피드백 제공에 이르기까지 많은 앱 개발자가 피드백을 받고 앱 스토어에 앱을 게시하는 데 한 달 지연을 경험했습니다. 더 짜증나는 것은 앱 문제를 해결하고 Apple로부터 다시 승인을받는 것입니다. 예를 들어 앱에 버그 또는 보안 취약성과 같은 문제가있는 경우 Apple에서 해결을 요청하고 앱 스토어 지침에 따라 앱을 적절하게 만들 것입니다. 최종적으로 모든 문제가 해결되면 앱 스토어에서 앱을 다시 제출하고 피드백을 기다려야합니다. 앱이 마침내 iTunes에 게시 될 때까지 몇 주 동안 기다려야합니다.

하지만 Apple App Store에서 맞춤형 앱을 배포하는 과정은 번거롭지 만 앱에 대한 최적의 가시성과 가용성이 필요한 경우 장기적으로는 최선의 선택이 될 것입니다. 그러나 Apple의 긴 앱 승인 프로세스를 없애고 싶다면 iTunes에서 앱을 배포하기 위해 구현할 수있는 몇 가지 다른 방법에 대해 설명합니다.

■ 임시 배포 (Ad-Hoc Deployment)

비공개 베타 또는 소규모 임시 배포 용 앱을 만든 경우 Apple의 Ad-Hoc 배포는 번거 로움없이 효율적으로 작업을 완료할 수 있습니다. 특히 앱 개발자의 경우 임시 배포에서 개발자는 이메일 서비스를 사용하거나 URL을 다운로드하거나 다른 서비스를 위해 앱 바이너리를 각 장치에 전달해야합니다. 하지만이 바이너리 코드가 모든 iOS 기기에서 작동하는 것은 아닙니다. 앱 개발자는 각 기기의 UDID를 추가하고 Apple Member Center에 각각을 등록하여 바이너리를 등록 된 기기에 쉽게 설치할 수 있도록해야합니다.

이 배포 방법을 사용하기 위해 앱 개발자는 Ad-Hoc 배포 옵션을 사용하여 Xcode에서 앱을 내보내기 만하면됩니다. 바이너리를 내 보내면 엔터프라이즈 바이너리와 마찬가지로 MDM을 통해 배포할 수도 있습니다. 그러나 MDM을 통해 배포하는 것이 기적적으로 바이너리의 라이선스를 더 관대하게 만들 수 없기 때문에 바이너리의 UDID를 관리하는 것이 주요 차이점입니다.

■ 애플 비즈니스 매니저 (Apple Business Manager)

Apple Business Manager 또는 ABM은 기업이 중앙 관리 지점에서 DEP (Device Enrollment Program), VPP (Volume Purchase Program), Apple ID 및 콘텐츠를 관리할 수있는 Apple 호스팅 클라우드 포털입니다. 포털은 관리자 위임을 통해 세분화 된 액세스 제어를 제공하므로 특정 위치에 대해서만 책임이있는 관리자를 만들 수 있습니다.

DEP 또는 VPP를 사용하고 있던 엔터티는 데스크톱을 사용하는 번거 로움없이 ABM으로 업그레이드할 수 있습니다. ABM을 사용하면 구입 후 Apple 장치를 자동으로 추가할 수 있습니다. 기기를 켜면 구성, 앱 및 책이 자동으로 설치됩니다. ABM에서는 앱과 책을 대량으로 구매할 때 직원이 사용하는 장치에 할당할 수 있습니다. 이러한 앱은 필요한 경우 다른 장치에 간단히 재할당할 수 있습니다.

포털에서는 조직의 모든 사무실에 대해 별도의 위치를 ​​만들고 관리자를 할당하고 자체 사무실에 대한 설정을 구성할 수도 있습니다.

Apple Business로 등록하여 Apple Business Manager를 사용하여 콘텐츠를 구입 및 배포하고 기기 배포를 자동화할 수 있습니다. 등록 프로세스를 완료하면 선택한 조직이나 사람이 앱을보고 Apple Business Manager의 콘텐츠 섹션에서 구입 한 다음 모바일 장치 관리 또는 MDM을 통해 원활하게 배포할 수 있습니다. 또한 조직은 App Store에서 앱을 다운로드할 수 있는 승인된 사용자에게 상환 코드를 제공할 수 있습니다.

  ▶ 엔터프라이즈 배포

  처음에는 아무도 엔터프라이즈 배포에 대해 생각한 적이 없었지만,이 앱 배포 방법은 기업이 앱 스토어 및 임시 배포의 까다로운 프로세스에 질 렸을 때 각광을 받았습니다. 앱 승인을 위해 몇 주를 기다리거나 모두 등록해야합니다. 내부 용도로 앱을 사용하는 경우에도 Apple Member Center의 장치 UDID.

엔터프라이즈 배포에서 앱은 일반적으로 Xcode에서 특정 방식으로 서명하고 내보내므로 기기의 UDID를 등록하거나 앱을 앱 스토어에 게시하는 번거 로움없이 모든 기기에 쉽게 설치할 수 있습니다. 이 앱 배포 방법이 인정을 받으면서 점점 더 많은 기업에서 조직 내에서 앱을 사용하기 시작했습니다. 엔터프라이즈 배포는 앱 스토어 및 임시 배포보다 훨씬 쉽지만이 배포 방법에서는 Apple이 U 턴을 취하고 조직 내 앱 배포에 대해 회사에 전적인 책임을 부여합니다.

이 배포 방법의 앱 배포는 이메일 또는 특정 URL을 통해 수행할 수 있는 임시 배포와 매우 유사합니다. 엔터프라이즈 배포는 MDM (모바일 장치 관리) 서비스를 통해 수행할 수도 있습니다. 앱 개발자는 바이너리를 업로드하고 단일 웹 관리 대시 보드를 통해 모든 MDM 등록 장치에 원격으로 설치 요청을 보내야합니다.

사실 엔터프라이즈 배포는 자체 앱을 만들고 직원간에 공유할 수있는 조직으로만 제한됩니다. 이러한 형태의 앱 배포는 조직 외부의 앱 배포를 위해 기업 서명 IPA를 사용하는 경우 Apple의 프로그램에 엄격히 위배됩니다.

  ▶ VPP Private Store B2B 앱 배포

  지금까지 전 세계의 앱 소유자와 조직이 구현 한 주요 앱 배포 방법에 대해 논의했습니다. 이제 거의 사용되지 않는 앱 배포 방법 중 하나 인 VPP Private Store B2B 앱 배포에 대해 살펴 보겠습니다.

Apple VPP에 등록하면 조직이 자신의 개인 앱 스토어를 가질 수 있습니다. 아니, 우리는 농담이 아닙니다! 이 앱 배포 프로그램에서는 내부 앱 바이너리를 쉽게 생성하고 프로그램에 참여하는 모든 기기와 공유할 수 있습니다. 또한 VPP를 통해 다른 사람과 앱을 공유할 수도 있습니다. 더 놀라운 것은 앱 승인 프로세스입니다.

아니요, 이 경우 당황할 필요가 없습니다. VPP Private Store B2B App Deployment의 앱 승인 프로세스는 앱 스토어 승인 프로세스와 비교할 때 훨씬 더 유연합니다. 앱 스토어와 같은 일반적인 용도로 앱을 사용할 수 없기 때문입니다. 또한 Apple VPP는 개인 저장소이므로 전체 앱 배포 작업이 프로그램 자체에서 처리되므로 수동 제출이 필요하지 않습니다. Apple의 VPP 프로그램은 기본적으로 MDM 서비스와 함께 사용되므로 조직은 단일 관리 대시 보드에서 장치로 VPP 푸시 초대와 앱을 보낼 수 있습니다.

위에서 설명한 앱 배포 방법 중 하나를 구현하면 작업이 올바른 방식으로 완료 될 수 있지만 프로그램 중 하나를 완료하고 진행하기 전에 앱의 실제 배포 프로세스를 미리 계획하는 것이 중요합니다. 적절하게 탐색하는 데 필요한 수백 개의 작은 세부 정보가 있습니다. 그렇지 않으면 힘들게 번 돈을 낭비하게 될 수 있습니다.

■ 결론

단순히 앱을 만드는 것만으로는 충분하지 않습니다. 청중에게 배포할 때 취할 경로를 명확하게 파악해야합니다. iOS 앱을 배포할 때 복잡한 경로로 보일 수 있지만 Appy Pie를 사용하면 상황이 훨씬 더 간단해질 수 있습니다.

처음부터 전체 앱을 만들뿐만 아니라 Apple App Store에 게시할 수 있도록 도와드립니다!

■ reddit 에 올라온 포스트 번역

(출처 : https://www.reddit.com/r/iOSProgramming/comments/fu4zdl/apple_business_manager_or_apple_enterprise/fmbtwnh/?context=8&depth=9)

작성자 : bakshioye

제목 : 내 고객을위한 조직의 직원 앱 배포를위한 Apple Business Manager 또는 Apple Enterprise 프로그램

우리 조직은 고객의 직원에게 배포하고 내부 용으로 배포해야하는 앱을 개발했습니다. 최근에 내 조직의 Apple 개발자 계정을 사용하여 앱을 업로드했지만 클라이언트 내부 용이라는 말을 거부 했으므로 Apple Business Manager (ABM)를 선호합니다.

ABM은 내 고객이 원하지 않는 MDM을 요구하기 때문에 (직원들이 개인 기기를 사용하기 때문에) Apple Enterprise Program에 가야합니까?

동일한 앱을 여러 클라이언트에 배포할 예정이므로 몇 가지 질문이있었습니다.

1. 내 조직이 Apple Enterprise 프로그램을 구매 한 다음 해당 (내 조직의 Enterprise) 계정을 사용하여 다양한 클라이언트에 앱을 배포할 수 있습니까? 아니면 각 클라이언트가 해당 계정을 명시 적으로 구매해야합니까?

2. 앱을 비공개로 배포할 수 있는 다른 방법이 있습니까? 아니면 ABM이 MDM없이 작동할 수 있습니까?

———-

webtechmonkey : 회사 직원에게 비공개 내부 앱을 배포하는 유일한 방법은 MDM을 이용하는 것입니다.

배포는 엔터프라이즈 앱에서 가장 까다로운 부분입니다. 기본적으로 여러분이 상상할 수 있듯이 Apple이 가능한 한 안전하게 잠그는 작업을 수행 한 공용 앱 스토어를 우회하기 때문입니다.

귀하의 직접적인 질문에 답하기 위해;

조금 더. 하지만 배포 문제는 해결되지 않습니다. TaviRider 님의 이전 제안에 따라 …. “[내부] 웹 사이트에서 IPA 및 매니페스트를 호스팅합니다. 사용자는 매니페스트에 대한 링크를 탭하고 확인한 다음 설정에서 엔터프라이즈 개발자 계정을 신뢰하기 만하면됩니다. . “

ABM은 MDM에서만 작동합니다. 이 문제를 해결할 방법이 없습니다.

고객은 MDM 사용을 진지하게 고려해야합니다. 시장에는 동일한 최종 목표를 달성하면서 상당히 저렴한 숫자가 있습니다. 또한 대부분의 MDM에서는 직원이 장치 감독없이 BYOD를 수핼할 수 있습니다 (즉, 고용주는 장치를 제어할 수 없지만 앱 설치를 요청할 수 있음).

* BYOD : Bring Your Own Device. 개인이 소유한 스마트 기기를 직장에 가져와 업무에 활용하도록 허용하는 정책

———-

bakshioye : 고마워요. 내 클라이언트는 비용이 아니라 필요한 설정을 위해 MDM을 피하고 있지만 추측합니다. 그들은 내가 생각하는 안드로이드만큼 간단한 것을 원합니다. 제 필요에 맞는 MDM을 추천 해 주시겠습니까?

———-

webtechmonkey : TaviRider 님의 좋은 제안으로 내 응답을 수정했습니다. 실제로 가능할 것입니다. 내가 개발 한 엔터프라이즈 앱이 우리 회사의 엔터프라이즈 인증서와 함께 있고 우리의 MDM을 통해 배포 되었기 때문에 나는 그것을 시도한 적이 없습니다.

저는 AirWatch를 오랫동안 사용해 왔습니다. 비교적 저렴하지만 (장치 당 월 4 달러 정도) 훌륭한 기능을 제공합니다.

Meraki System Manager는 또 다른 훌륭한 솔루션이지만 비용이 조금 더 듭니다.

Jamf가 훌륭하다고 들었지만 직접 사용하지 않았으며 가격이 어떻게 보이는지 확실하지 않습니다.

———-

bakshioye : 네, 들어 본 적 있는데 정리하고 싶었어요. 내가 알아낼 수 있는 것은 다음과 같습니다.

Apple이 내 앱을 확인하고 확인 후 IPA 파일을 제공합니다.

조직 직원이 앱을 다운로드할 수있는 내부 링크의 해당 IPA 파일을 호스트하는 조직

이 과정이 정확합니까?

———-

TaviRider : # 2는 대부분 맞지만 # 1은 틀렸습니다. 엔터프라이즈 앱 배포의 경우 엔터프라이즈 개발자 계정과 해당 계정에서 배포 서명 ID를 얻습니다. Apple에서 앱에 대한 프로비저닝 프로파일을받습니다. 앱을 빌드하고 프로비저닝 프로필을 포함하고 서명 ID로 서명 한 다음 IPA로 배포할 수 있도록 패키지화합니다. 그런 다음 매니페스트 파일을 만들고 웹 사이트에서 IPA 및 매니페스트 파일을 호스팅합니다. 사용자는 Safari에서 매니페스트에 대한 링크를 클릭하여 앱을 설치합니다. 처음 실행하기 전에 사용자는 설정에서 개발자 계정을 신뢰해야합니다. Apple은 장치에 ID, 서명 및 프로비저닝 프로파일이 유효한지 확인하는 유효성 검사 단계가 있지만 IPA를 Apple에 보내는 것은 포함되지 않습니다.

———-

bakshioye : 그렇다면 앱 스토어 배포 용 앱처럼 앱을 확인 / 검증하지 않는다는 말씀이신가요? 프로비저닝 프로필로 빌드하고 서명하면 배포할 준비가 되었나요? IPA 및 매니페스트를 URL에 업로드하고, 사용자가 매니페스트 파일을 열도록하고, 사용자가 개발자 계정을 신뢰하면 장치에 다운로드됩니까?

———-

TaviRider : 맞습니다.

앱은 여전히 ​​Apple 개발자 계약의 규칙을 준수해야합니다. 앱 스토어 검토 프로세스를 건너 뜁니다. 귀하가 개발자 계약을 위반했음을 발견하면 Apple이 귀하의 계정을 해지할 수 있으며 이에 따라 모든 앱의 작동이 중지됩니다.

———-

bakshioye : 마지막으로. 이 엔터프라이즈 계정은 클라이언트 자체에서 구매해야하거나 내 조직이 계정을 구매 한 다음 내 클라이언트에 앱을 출시 / 배포할 수 있습니다 (여러 클라이언트 / 조직이있을 것임).

.

언어별 diff 라이브러리 정보 (텍스트 비교 라이브러리 정보)

언어별 diff 라이브러리 정보 (텍스트 비교 라이브러리 정보)

그동안 자체 개발한 diff 알고리즘을 쓰고 있었는데 역시 검증된 라이브러리로 교체하는 것이 좋을 것 같다.

JAVA 쪽만 훝어봤는데 tests 패키지 안의 테스트코드를 예제코드로 보면 되고 이해하기도 쉽다.

google-diff-match-patch

지원 언어 : C++, C#, Dart, Java, JavaScript, Lua, Objective-C, Python

GITHUB : https://github.com/google/diff-match-patch

참고사이트 : https://itzone.tistory.com/380

[HTML] http 페이지와 https 페이지의 iframe 처리

[HTML] http 페이지와 https 페이지의 iframe 처리

http 페이지 안에 https 페이지를 아이프레임으로 넣는 것은 가능하지만,
https 페이지 안에 http 페이지를 아이프레임으로 넣는 것은 크롬 브라우저 보안정책상 불가능.

[Vue.js] import 상대경로를 절대경로로 변경

[Vue.js] import 상대경로를 절대경로로 변경 

Vue.js 에서 일반적인 import 문은 다음과 같은 형태를 갖는다.

import CommonUtil from ‘../../resources/js/common’;

이렇게 js 또는 vue 파일의 경로를 상대경로로 적으면 가독성이 떨어지고, 다른 파일에 붙여넣어서 작업할 때 일일히 경로를 보정해줘야 하므로 오류성도 높다.

Vue.js 에서 import 또는 require 구문의 상대경로를 절대경로로 변경하려면 webpack.config.js 파일을 수정해야한다.

webpack.config.js

[AS-IS]

resolve: {
    alias: {
        ‘vue$’: ‘vue/dist/vue.esm.js’
    },
    extensions: [‘*’, ‘.js’, ‘.vue’, ‘.json’]
},

[TO-BE]

resolve: {
    alias: {
        ‘vue$’: ‘vue/dist/vue.esm.js’,
        ‘~’: path.resolve(__dirname, ‘src’)
    },
    extensions: [‘*’, ‘.js’, ‘.vue’, ‘.json’]
},

이제 Vue 파일 내의 import 구문을 아래와 같이 바꿔 사용할 수 있다.

[AS-IS]

import CommonUtil from ‘../../resources/js/common’;

[TO-BE]

import CommonUtil from ‘~/resources/js/common’;

참고사이트 : https://wonism.github.io/resolve-import-path/ 

[Vue.js] 페이지 새로고침 함수

[Vue.js] 페이지 새로고침 함수

SPA(Single Page Application)을 만들었다면 일반적인 새로고침 방식(ex : location.href = location.href)은 권장되지 않는다.

Vue.js 에서 페이지 새로고침은 this.$router.go(); 로 한다.

<script>
export default {
    methods : {
        refreshAll() {
            // 새로고침
            this.$router.go();
        }
    }
};
</script>

[Javascript] function parseIntForce

[Javascript] function parseIntForce

직접 만든 함수.

    // 간편한 parseInt
    parseIntForce : function(_num) {
        if (_num == null || _num == “”) {
            return 0;
        }
       
        _num = _num + “”;
        _num = _num.replace(“px”, “”);
        _num = _num.replace(“PX”, “”);
       
        var result = parseInt(_num, 10);
        if (result == “NaN”) {
            return 0;
        }
       
        return result;
    }

[Javascript] function devideBy

[Javascript] function devideBy

직접 만든 함수.

    // 무조건 정수를 얻는 나누기
    devideBy : function(_target, _num) {
        if (_target == null || _target == “”) {
            return 0;
        }
       
        var target = parseInt(_target, 10);
        if (target == “NaN” || target < 1) {
            return 0;
        }
       
        if (_num == null || _num == “”) {
            return 0;
        }
       
        var num = parseInt(_num, 10);
        if (num == “NaN” || num < 1) {
            return 0;
        }
       
        if (target < num) {
            return 0;
        }
       
        var remain = target % num;
        var quot = target – remain;
        if (quot < 1) {
            return 0;
        }
       
        return quot / num;
    }

[Eclipse] A cycle was detected in the build path of project

[Eclipse] A cycle was detected in the build path of project

이클립스 Markers 탭에 아래와 같은 오류 발생한 경우.

Java Build Path Problems
– A cycle was detected in the build path of project ‘projectName1’. The cycle consists of projects {projectName1, projectName2}

■ 해결방법

이클립스 상단메뉴 [WIndow] – 하위의 [Preferences] – Preferences 창이 뜨면 좌측 트리의 [Java] 항목 – 하위의 [Compiler] – 하위의 [Building] – 우측 영역의 [Circular dependencies] 콤보박스 값을 [Warning] 으로 변경 후 [OK] 버튼 클릭

참고사이트 : https://blog.naver.com/ah_hwal/220118774957

[Spring] 서블릿 컨텍스트 패스(ex : webapp 폴더) 실제경로 가져오기

[Spring] 서블릿 컨텍스트 패스(ex : webapp 폴더) 실제경로 가져오기

1. 스프링 컨트롤러(@Controller) 또는 서블릿에서 컨텍스트 패스(ex : webapp 폴더) 실제경로를 알아내고 싶다면 아래처럼 HttpServletRequest 객체를 이용해서 가져온다.

참고로 request.getSession().getServletContext() 를 쓰거나 request.getServletContext() 쓰거나 동일한 값을 가져온다.

@RequestMapping(value = “/”, method = {RequestMethod.GET,RequestMethod.POST})

public void test(HttpServletRequest request) {

    ServletContext servletContext = request.getSession().getServletContext();
    String realPath = servletContext.getRealPath(“/”);
}

2. 스프링 컨트롤러(@Controller)가 아닌 경우(서블릿이 아닌 경우) HttpServletRequest 객체를 파라미터로 넘겨줘야 한다.

3. 스프링 컨트롤러가 초기화될 때(@PostConstruct) 컨텍스트 패스를 알아내고 싶다면 ServletContext 를 @Autowired 로 받아서 사용한다.

@Controller
public class TestController {
    @Autowired
    private ServletContext servletContext;
 
    @PostConstruct
    public void initialize() {
        String realPath = servletContext.getRealPath(“/”);
    }
}

첨언하자면 getRealPath()의 리턴값 마지막 문자로 슬래시 또는 역슬래시가 오는지 여부는 OS 및 WAS 환경을 타는 것 같다. 로컬환경에서는 마지막 문자로 역슬래시가 있었는데, 운영환경(윈도우 서버였음)에서는 마지막 문자로 역슬래시가 오지 않는 현상이 있었다(믿기 어렵지만 사실이다). 따라서 마지막 문자로 슬래시 또는 역슬래시가 오기를 원한다면 코드 상에서 명시적으로 붙여주도록 한다.

예를 들어 아래와 같이 코딩하면 된다.

String realPath = servletContext.getRealPath(“/”);

if (!realPath.endsWith(File.separator)) {
    realPath = realPath + File.separator;
}

참고사이트 1 : https://sourcestudy.tistory.com/360

[Nginx] 리눅스 shutdown 이후에 nginx restart 했을 때 접속불가 현상

[Nginx] 리눅스 shutdown 이후에 nginx restart 했을 때 접속불가 현상

nginx 가 잘 동작하던 상태에서, 리눅스 shutdown 이후에 서버 주소를 입력해도 접속되지 않는 현상.

예를 들어 우분투에서 sudo shutdown now -r 를 입력하고 리눅스 재부팅 후, sudo service nginx relaod 그리고 sudo service nginx restart 했을 때 접속되지 않는 경우.

ps -ef | grep nginx 로 확인해보면 분명 nginx가 떠있는데 access.log (ex : /var/log/nginx/access.log) 에도 아무 문자열이 찍히지 않는 경우.

터미널에 sudo iptables -F 를 입력하면 해결된다.

현재 iptables 규칙을 초기화하는 명령어이다.

[Oracle Cloud] 소유중인 도메인을 Oracle Cloud 서비스로 연결하기 (오라클 클라우드 도메인 연결 방법)

[Oracle Cloud] 소유중인 도메인을 Oracle Cloud 서비스로 연결하기 (오라클 클라우드 도메인 연결 방법)

구입한 도메인이 있을 경우, 해당 도메인 주소로 접속했을 때 Oracle Cloud(오라클 클라우드) 서비스로 연결하는 방법이다.

조건 1) 소유하고 있는 도메인이 있음

조건 2) Oracle Cloud(오라클 클라우드) 인스턴스에 띄워져 있는 웹서비스가 있음

참고로 서버 고정아이피는 미리 만들어뒀어야 하고, 인스턴스와 연결해둔 상태여야 한다.

다시 말해 인터넷 브라우저 주소창에 고정아이피를 입력하면 본인의 웹서비스와 연결 가능한 상태여야 한다.

혹시 고정아이피를 인스턴스와 연결해두지 못한 분들을 위해 간략하게 정리하면 다음과 같다.

(참고사이트 : https://itreport.tistory.com/625 )

(1) 서버 고정아이피 생성 방법

좌측상단 메뉴 아이콘 – [네트워킹] 메뉴 – 좌측의 [공용IP] 메뉴 – [예약된 공용 IP 생성] 버튼 클릭 – [이름] 항목에 임의의 이름 작성 – [예약된 공용 IP생성 버튼] 클릭

(2) 서버 고정아이피를 인스턴스와 연결 방법

좌측상단 메뉴 아이콘 – [컴퓨트] 메뉴 – [인스턴스] 메뉴 – 좌측의 [연결된 VNIC] 메뉴 – 생성되어 있는 VNIC 클릭해서 세부정보 조회 – 좌측하단 [IP 주소] 메뉴 클릭 – [공용 IP 없음] 라디오 버튼 클릭 후 [업데이트] 버튼 클릭 – 같은 화면에서 [예약된 공용 IP] 라디오 버튼 클릭 – [예약된 공용 IP] 콤보박스에서 (1)에서 생성한 IP 선택 – [업데이트] 버튼 클릭

만약 오라클 클라우드가 아닌 AWS 를 사용한다면 다음 포스트를 참고하면 된다.

[AWS] 소유중인 도메인을 AWS EC2 서비스로 연결하기 (AWS 도메인 연결 방법) (https://blog.naver.com/bb_/222098004373)

1. 오라클 클라우드 콘솔에 접속한다. 좌측상단의 메뉴 아이콘 – [네트워킹] – [DNS 관리] 메뉴를 클릭한다.


2. [영역 관리] 버튼을 클릭한다.


3. [영역 생성] 버튼을 클릭한다.



4. [공용 영역 생성] 창이 뜨면 [영역이름] 항목에 본인이 소유하고 있는 도메인을 입력한다. (ex : example.com)

하단의 [생성] 버튼을 클릭한다.



5. 좌측의 [레코드] 메뉴를 클릭한다.



6. [레코드] 항목의 [레코드 추가] 버튼을 클릭한다.



7. [레코드 유형] 콤보박스는 [A – IPv4 주소] 항목을 클릭한다.

[ADDRESS(주소)] 는 서버 고정아이피를 입력한다.

[제출] 버튼을 클릭하면 [TTL] 항목을 입력할 수 있도록 커서가 이동한다. [30]을 입력하고 다시 [제출] 버튼을 클릭하면 된다.

참고로 서버 고정아이피는 미리 만들어뒀어야 하고, 인스턴스와 연결해둔 상태여야 한다.

다시 말해 인터넷 브라우저 주소창에 고정아이피를 입력하면 본인의 웹서비스와 연결 가능한 상태여야 한다.



8. [변경사항 게시] 버튼을 클릭한다.



9. [변경사항 게시] 버튼을 클릭한다.


 

10. 마지막으로 도메인 호스팅 사이트에 네임서버 값을 입력해야 한다.

우선 오라클 클라우드에서 네임서버를 확인한다.


 

다음으로 도메인 호스팅 사이트에 네임서버 값을 입력하면 된다.

예를 들어 가비아에 도메인을 등록했다면, [네임서버] 항목의 [설정] 버튼을 클릭하고 해당 네임서버 4개를 기입하면 된다.

아래 스크린샷을 참고할 것.



 

11. 이제 인터넷 브라우저 주소창에 본인 소유의 도메인을 입력했을 때 본인의 웹서비스와 연결될 것이다.

‘wimc’은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.

‘wimc’은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.

윈도우 명령 프롬포트(cmd) 에서 wimc 를 실행했을 때 “‘wimc’은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.” 메시지가 표시되는 경우.

해결방법 1. 실제 경로를 입력해서 사용한다.

wimc 만 입력하는게 아니라 풀 패스 (실제 경로인 C:\Windows\System32\wbem\wimc) 를 입력해서 사용한다.

해결방법 2. 시스템 변수에 wimc 폴더 경로를 추가한다.

[내 PC] 마우스 우클릭 – [속성] – [고급 시스템 설정] – [고급] 탭 – [환경 변수] 버튼 클릭 – [시스템 변수] 항목의 [Path] 값 선택하고 [편집] 버튼 클릭 해서 C:\Windows\System32\wbem 경로를 추가한다.

참고사이트 : https://stackoverrun.com/ko/q/10841590

[Android] android.os.NetworkOnMainThreadException 오류

[Android] android.os.NetworkOnMainThreadException 오류

네트워크 통신을 메인 쓰레드에서 호출할 경우 발생하는 오류임.

네트워크 통신은 작업 쓰레드로 분리하여 처리하든지 AsynkTask 클래스를 작업하여 처리해야 함.

참고사이트 : https://sunghodev.tistory.com/11

[Javascript] HTML 요소의 실제 width, 실제 height

[Javascript] HTML 요소의 실제 width, 실제 height

HTML 요소의 실제 너비(width) 및 실제 높이(height)는 offsetWidth, offsetHeight 로 구한다.

ex) document.getElementsByTagName(“body”)[0].offsetWidth

document.getElementsByTagName(“body”)[0].offsetHeight

참고사이트 : https://c10106.tistory.com/3233

[Android] 안드로이드 하단바, 하단버튼 붙이는 방법

[Android] 안드로이드 하단바, 하단버튼 붙이는 방법

안드로이드 웹뷰를 사용하는 액티비티 하단에 버튼 4개를 붙여본다.

1. 우선 drawable-hdpi 폴더 안에 이미지 파일(png 파일) 5개를 추가한다.

ex) 프로젝트폴더\app\src\main\res\drawable-hdpi

(1) button_home_android.png

홈 버튼 이미지. 크기는 가로 45px, 세로 45px 로 한다.

(2) button_back_android.png

뒤로가기 버튼 이미지. 크기는 가로 45px, 세로 45px 로 한다.

(3) button_forward_android.png

앞으로가기 버튼 이미지. 크기는 가로 45px, 세로 45px 로 한다.

(4) button_refresh_android.png

새로고침 버튼 이미지. 크기는 가로 45px, 세로 45px 로 한다.

(5) menu_background_black.png

배경 이미지. 순수 검은색 배경이면 된다. 가로 5px, 세로 60px로 한다.

2. drawable-hdpi 폴더 안에 버튼 xml 파일 4개를 만든다.

ex) 프로젝트폴더\app\src\main\res\drawable-hdpi

(1) menu_home.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<selector xmlns:android=”http://schemas.android.com/apk/res/android“>
    <item android:state_selected=”true” android:drawable=”@drawable/button_home_android” /><!– selected –>
    <item android:state_pressed=”true” android:drawable=”@drawable/button_home_android” /><!– selected –>
    <item android:state_focused=”true” android:drawable=”@drawable/button_home_android” /><!– focused –>
    <item android:drawable=”@drawable/button_home_android” /><!– default –>
</selector>

(2) menu_back.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<selector xmlns:android=”http://schemas.android.com/apk/res/android“>
    <item android:state_selected=”true” android:drawable=”@drawable/button_back_android” /><!– selected –>
    <item android:state_pressed=”true” android:drawable=”@drawable/button_back_android” /><!– selected –>
    <item android:state_focused=”true” android:drawable=”@drawable/button_back_android” /><!– focused –>
    <item android:drawable=”@drawable/button_back_android” /><!– default –>
</selector>

(3) menu_forward.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<selector xmlns:android=”http://schemas.android.com/apk/res/android“>
    <item android:state_selected=”true” android:drawable=”@drawable/button_forward_android” /><!– selected –>
    <item android:state_pressed=”true” android:drawable=”@drawable/button_forward_android” /><!– selected –>
    <item android:state_focused=”true” android:drawable=”@drawable/button_forward_android” /><!– focused –>
    <item android:drawable=”@drawable/button_forward_android” /><!– default –>
</selector>

(4) menu_refresh.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<selector xmlns:android=”http://schemas.android.com/apk/res/android“>
    <item android:state_selected=”true” android:drawable=”@drawable/button_refresh_android” /><!– selected –>
    <item android:state_pressed=”true” android:drawable=”@drawable/button_refresh_android” /><!– selected –>
    <item android:state_focused=”true” android:drawable=”@drawable/button_refresh_android” /><!– focused –>
    <item android:drawable=”@drawable/button_refresh_android” /><!– default –>
</selector>

3. 이제 웹뷰(WebView)를 사용하는 액티비티(Activity) 파일의 레이아웃 파일명을 알아내야 한다.

해당 액티비티(Activitty) 파일에서 setContentView 라는 메서드를 찾아보면 레이아웃 파일명을 알 수 있다.

public class TestActivity extends Activity {

    (중략)

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
   

        // 기존코드 예시

        setContentView(R.layout.webview_container);

        (중략)
    }

    (중략)

}

이어서 해당 액티비티의 레이아웃 파일을 수정한다.

(ex : 프로젝트폴더\app\src\main\res\layout\webview_container.xml)

[AS-IS]

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android
    android:layout_width=”fill_parent” android:layout_height=”fill_parent”
    android:scrollbars=”vertical”
    android:scrollbarAlwaysDrawVerticalTrack=”true”>
    <LinearLayout android:orientation=”vertical”
        android:layout_width=”fill_parent” android:background=”@color/default_background_color”
        android:layout_height=”fill_parent” android:scrollbars=”vertical”
    android:scrollbarAlwaysDrawVerticalTrack=”true”>
   
        <WebView android:id=”@+id/webView” android:layout_width=”fill_parent”
            android:layout_height=”fill_parent” android:layout_weight=”1″
            android:scrollbars=”vertical”
            android:scrollbarAlwaysDrawVerticalTrack=”true”   
            android:scrollbarStyle=”insideOverlay” />

    </LinearLayout>
</LinearLayout>

 

[TO-BE]

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android
    android:layout_width=”fill_parent” android:layout_height=”fill_parent”
    android:scrollbars=”vertical”
    android:scrollbarAlwaysDrawVerticalTrack=”true”>
    <LinearLayout android:orientation=”vertical”
        android:layout_width=”fill_parent” android:background=”@color/default_background_color”
        android:layout_height=”fill_parent” android:scrollbars=”vertical”
    android:scrollbarAlwaysDrawVerticalTrack=”true”>
   
        <WebView android:id=”@+id/webView” android:layout_width=”fill_parent”
            android:layout_height=”fill_parent” android:layout_weight=”1″
            android:scrollbars=”vertical”
            android:scrollbarAlwaysDrawVerticalTrack=”true”   
            android:scrollbarStyle=”insideOverlay” />

        <LinearLayout android:id=”@android:id/tabs”
            android:layout_width=”fill_parent” android:layout_height=”wrap_content”
            android:background=”@drawable/menu_background_black”>


            <ImageButton
                android:id=”@+id/menuHome”
                android:layout_width=”wrap_content”
                android:layout_height=”wrap_content”
                android:layout_weight=”1″
                android:background=”@drawable/menu_background_black”
                android:src=”@drawable/menu_home”></ImageButton>


            <ImageButton android:background=”@drawable/menu_background_black”
                android:src=”@drawable/menu_back” android:id=”@+id/menuBack”
                android:layout_width=”wrap_content” android:layout_weight=”1″
                android:layout_height=”wrap_content”></ImageButton>


            <ImageButton android:background=”@drawable/menu_background_black”
                android:src=”@drawable/menu_forward” android:id=”@+id/menuFoward”
                android:layout_width=”wrap_content” android:layout_weight=”1″
                android:layout_height=”wrap_content”></ImageButton>


            <ImageButton android:background=”@drawable/menu_background_black”
                android:src=”@drawable/menu_refresh” android:id=”@+id/menuRefresh”
                android:layout_width=”wrap_content” android:layout_weight=”1″
                android:layout_height=”wrap_content”></ImageButton>
        </LinearLayout>

    </LinearLayout>
</LinearLayout>

 

4. 다음으로 웹뷰(WebView)를 갖고 있는 액티비티(Activity) 파일의 코드를 수정해야 한다. (ex : public class TestActivity extends Activity)

해당 액티비티 상단에 버튼 변수를 정의한다.

public class TestActivity extends Activity {

   

    // 기존코드 예시

    public WebView webview = null;

   

    // 하단바 버튼 추가

    public ImageButton menuHome, menuBack, menuForward, menuRefresh;

   

    (중략)

}

5. 해당 액티비티 onCreate 메서드를 작성한다. 각 버튼에 클릭 리스너를 세팅한다.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
   

    // 기존코드 예시

    setContentView(R.layout.webview_container);

    (중략)

   

    // 하단바 버튼 추가
    menuHome = (ImageButton) findViewById(R.id.menuHome);
    menuBack = (ImageButton) findViewById(R.id.menuBack);
    menuForward = (ImageButton) findViewById(R.id.menuFoward);
    menuRefresh = (ImageButton) findViewById(R.id.menuRefresh);


    menuHome.setOnClickListener(this);
    menuBack.setOnClickListener(this);
    menuForward.setOnClickListener(this);
    menuRefresh.setOnClickListener(this);

    (중략)

    // 기존코드 예시

    webview = (WebView) findViewById(R.id.webView);

    webview.postUrl(“http:// 홈페이지주소”, null);
}

6. 해당 액티비티 onClick 메서드를 작성한다. 버튼 클릭시 기능을 구현한다.

@Override
public void onClick(View v) {
    int id = v.getId();

    // 하단바 버튼 추가
    if (id == R.id.menuBack) {
        if (webview.canGoBack()) {
            webview.goBack();
        } 


    } else if (id == R.id.menuFoward) {
        if (webview.canGoForward()) {
            webview.goForward();
        } 


    } else if (id == R.id.menuRefresh) {
        webview.reload();

    } else if (id == R.id.menuHome) {
        webview.loadUrl(“http:// 홈페이지주소”);
    }

}

[iOS] xib 하단바, 하단버튼 붙이는 방법

[iOS] xib 하단바, 하단버튼 붙이는 방법

xib 에 웹뷰(WebView) 가 주어져 있을 때, 해당 웹뷰 하단에 버튼을 붙이는 방법.

정확히 표현하면 xib 파일에 하단바를 붙이고 그 안에 버튼을 추가하는 방법.

1. xib 에 웹뷰(WebView) 를 추가한 상태에서 시작한다.

 

2. Xcode 상단메뉴의 [View] – [Show Library] 메뉴를 클릭한다.

3. [Toolbar] 를 선택한다.

 

4. xib 좌측 목록에 Toolbar 가 추가되었으나 보이지는 않는다. 뒤에 가려져 있어서 그렇다.

 

5. 좌측 목록의 Toolbar 를 WebView 아래로 붙이면 우측 화면에 표시된다.

우측 화면에서 마우스 드래그앤드랍으로 Toolbar 를 하단에 딱 맞게 이동시키고, 가로 길이를 딱 맞도록 조절한다.

Item 이라는 글자가 보일텐데, 이것은 미리 추가되어 있는 Bar Button Item 이다.

여기까지 됐으면 앱을 한 번 디버그 모드로 실행해본다.

아이폰을 가로/세로 전환해도 화면 하단에 버튼 바가 붙어있어야 한다.

 

6. 다시 Xcode 상단메뉴의 [View] – [Show Library] 메뉴를 클릭해서, (1) Bar Button Item 과 (2) Flexible Space Bar Button Item 을 번갈아 추가한다.

버튼을 4개로 만들어보자.

현재 Bar Button Item 이 1개 미리 들어있으므로, (2) – (1) – (2) – (1) – (2) – (1) 순으로 추가하면 될 것이다.

추가순서가 헷갈린다면 일단 추가해놓고 왼쪽 목록에서 드래그앤드랍을 통해 추후 순서를 조정할 수 있다.



 

7. xib 좌측 목록이 아래와 같이 나오면 된다.

 

8. 첫번째 Item (=첫번째 Bar Button Item) 을 선택하고 텍스트를 이미지로 변경해보자.

우측 속성탭(Attributes inspector 라고 부름)의 Title 값을 지우자. (Item => 빈값)

이어서 Image 를 선택해야 하는데, 확장자가 표시되어 있는 이미지 (ex : button_back.png) 를 선택하면 잘 추가된 것처럼 보여도 나중에 앱을 실행하면 이미지가 표시되지 않는다.

Image Set 에 해당하는 항목들을 선택해야 한다. Image Set 이란 xcassets 파일에서 확인할 수 있다.

 

9. 기존에 원하는 이미지가 없다면 이미지를 추가해보자.

xcassets 파일 (ex : Resources 폴더의 Images.xcassets 파일) 을 더블클릭해서 열자.

 

10. 좌측 목록 위에서 마우스 우클릭해서 [Image Set] 버튼을 클릭하면 새로 Image Set 을 만들 수 있다.

이후 좌측목록으로부터 이미지를 화면 빈칸(이미지 슬롯)에 드래그앤드랍하면 된다.

참고로 이미지 크기는 가로 60px 세로 60px 크기로 만들었다.

11. 앱 실행 시 이미지가 앱에서 표시되지 않고 단색(ex : 파란색)으로 표시되는 경우가 있다.

해당 Image Set 을 선택하고 우측 속성탭(Attributes inspector)의 Render As 값을 [Original Image] 로 변경하고 앱을 다시 실행하면 된다.

 

12. xib 파일의 Bar Button Item 에 이미지를 적용한 (Image Set 을 적용한) 모습이다.

마우스 우클릭하면 Sent Actions – selector 라는 메뉴가 보인다.

 

13. Bar Button Item 를 선택하고 마우스 우클릭해서 Sent Actions – selector 의 우측 끝의 점을 클릭한다.

해당 점을 클릭한채로 화면 중앙의 웹뷰(WebView) 로 마우스 드래그를 하면 아래 그림처럼 파란색 선이 표시된다.

 

14. Bar Button Item 의 selector 와 웹뷰(WebView) 가 파란색 선으로 이어진 상태에서 마우스를 떼면, [goBack], [goForward], [reload], [stopLoading] 이 표시된다. [goBack] 을 선택하자.

이제 해당 버튼을 클릭하면 웹뷰(WebView)의 뒤로가기(goBack) 기능이 실행되도록 연결되었다.

웹뷰의 뒤로가기, 앞으로가기, 새로고침, 중지 등의 명령을 이와 같이 버튼에 매핑시킬 수 있다.

 

15. 자연스러운 이미지 처리를 위해 버튼바를 검은색으로 바꿔보도록 한다.

화면의 Toolbar 객체를 선택하고 우측 속성탭(Attributes inspector)의 style 을 [Black] 으로 선택, Translucent 체크박스를 체크 해제한다.

Translucent 체크박스가 체크되어 있으면 반투명이 된다.

 

16. 버튼 클릭시 이벤트를 웹뷰(WebView)의 메서드가 아니라 직접 만든 메서드에 매핑하고 싶은 경우.

우측 화면 추가(Add Editor on Right) 버튼을 클릭해서 창을 2개로 나눈다.

이후 좌측 창에는 xib 파일을, 우측 창에는 헤더 파일(확장자 h)을 연다. (ex : 좌측은 MainView.xib 를 열고, 우측은 MainView.h 파일을 열기)

이후 xib 화면에서 버튼을 선택하고 마우스 우클릭해서 selector 끝의 점을 클릭한 채로, 우측 창 헤더 파일(확장자 h)의 메서드에 마우스 드래그한다.

파란색 선이 표시된 후 마우스를 떼면 버튼에 원하는 메서드가 매핑된다.

참고사이트 1 : https://blog.yagom.net/231/

참고사이트 2 : https://stackoverflow.com/questions/40102554/image-is-not-showing-up-for-tab-bar-item

[iOS] objective-c NSString substring (substringFromIndex, substringToIndex)

[iOS] objective-c NSString substring (substringFromIndex, substringToIndex)

objective-c NSString 은 (1) 특정 인덱스부터 자르기와 (2) 특정 인덱스까지 자르기가 있다.

1. NSString 특정 인덱스부터 자르기 (substringFromIndex)

NSString *str = @”HelloWorld!”;


NSRange range1 = [str rangeOfString:@”W”];

NSUInteger idx = range1.location;

if ((int)idx > –1) {

    str = [str substringFromIndex: idx];

}

// result : World!

2. NSString 특정 인덱스까지 자르기 (substringToIndex)

NSString *str = @”HelloWorld!”;


NSRange range1 = [str rangeOfString:@”W”];

NSUInteger idx = range1.location;

if ((int)idx > –1) {

    str = [str substringToIndex: idx];

}

// result : Hello

[iOS] Xcode objective-c NSLog, printf 사용법

[iOS] Xcode objective-c NSLog, printf 사용법

■ NSLog

(1) 문자열 출력

NSLog(@”문자열”);

(2) 문자열 변수 출력

NSString *str = @”문자열”;

NSLog(@”str : %@”, str);

(3) 숫자 변수 출력

숫자(int 또는 NSUInteger)를 문자열로 변환해서 찍으면 된다.

int i = 10;

NSString *str = [NSString stringWithFormat:@”%d”, i];

NSLog(@”str : %@”, str);

■ printf

(1) 문자열 변수 출력

printf(“str : %s\n”, [str UTF8String]);

Xcode 프로젝트에서 NSLog 는 Build Configuration 이 Debug 일 때만 찍히고, printf 는 Debug 와 Release 상관없이 다 찍힌다.

다시 말해 NSLog 가 콘솔에 찍히지 않는다면 Build Configuration 을 Debug 로 변경하면 된다(https://blog.naver.com/bb_/221987953889).

참고사이트 : https://clack.tistory.com/242

[Xcode] resource fork, Finder information, or similar detritus not allowe

[Xcode] resource fork, Finder information, or similar detritus not allowed

잘 빌드되던 Xcode 프로젝트가 아래처럼 오류 발생하며 빌드되지 않는 경우.


resource fork, Finder information, or similar detritus not allowed

Command /usr/bin/codesign failed with exit code 1

아래와 같이 해본다.

1. command + shift + k

2. command + shift + option + k

3. 터미널에서 프로젝트 위치로 이동 (ex : cd /프로젝트위치)

4. xattr -cr . 입력

5. 다시 빌드

참고사이트 : https://stackoverflow.com/questions/39652867/code-sign-error-in-macos-high-sierra-xcode-resource-fork-finder-information

Apache/Tomcat HTTPS 사용 시 톰캣으로 리다이렉트하지 못하는 문제

Apache/Tomcat HTTPS 사용 시 톰캣으로 리다이렉트하지 못하는 문제

기존에 Apache/Tomcat 을 잘 사용하고 있는 기관이었고, 기존 Tomcat 을 무시하고 새로운 Tomcat 을 바라보도록 작업해야 하는 과제였다.

Apache 는 443 포트 사용(즉 https 프로토콜 사용), Tomcat 은 8080 포트 사용, AJP포트는 8009 사용하는 상황이었다.

아파치의 workers.properties 에는 worker.worker1.port=8009 가 잘 적혀있었고, Tomcat의 server.xml 에는 <Connector port=”8009″ URIEncoding=”UTF-8″ protocol=”AJP/1.3″ redirectPort=”18443″ /> 라고 잘 적혀있었다.

아파치 httpd.conf 의 DocumentRoot 값을 새 값으로 잘 바꿔줬고, Directory 태그 값도 새 값으로 잘 바꿔줬다. JkMount 값은 그대로 유지했고, (JkMount /* worker1) 잘못될 이유는 없어보였다.

문제는, 톰캣으로 다이렉트로 접근했을 때는(http ://도메인주소:8080/특정주소 를 입력하면) 페이지가 잘 뜨는데, 아파치를 통해서 접근하면(https ://도메인주소/특정주소 를 입력하면) 페이지가 나오지 않는 문제였다.

이때 Apache 를 443 포트 대신 80 포트를 사용하도록 수정하면, 톰캣으로 다이렉트로 접근했을 때도(http ://도메인주소:8080/특정주소 를 입력해도) 페이지가 잘 뜨고, 아파치를 통해서 접근했을 때도(http ://도메인주소:80/특정주소 를 입력해도) 페이지가 잘 나왔다.

단적으로 443 은 안되는데 80 은 되니까 Apache 에 인증서 문제라고 파악해서 한참을 검색하고 헤맸는데, 사실 인증서는 제대로 씌워진 상태였다.

상급자에게 전화로 도움을 요청한 결과, 로그를 봐야한다는 것을 깨달았다. 용량이 커서 로그를 열 수 없다고만 생각했는데, 기존 로그를 백업해두고 새로 쌓인 로그를 열면 되는 일이었다. 참고로 아파치든 톰캣이든 로그가 쌓여야 하는데 쌓이지 않으면 해당 서비스를 중지하면 된다. 일정분량이 되지 않으면 로그를 쓰지 않는 경우가 있는데, 서비스를 강제로 중지하면 flush가 일어나면서 파일이 써지기 때문이다.

일단 jk_module 을 통해(AJP 포트를 통해) 아파치로 간 요청이 톰캣에 가는지가 중요한 문제였으므로, 톰캣의 엑세스 로그를 봤다(ex : tomcat7\logs\localhost_access_log.2020-11-27.txt). 확인결과 443 을 통해 주소를 요청하면(아파치를 통해서 접근하면) 엑세스 로그가 전혀 써지지 않았다. 결국 아파치 단에서 문제가 생겼다는 뜻이었다.

아파치 톰캣 연동은 jk_module 을 통해(AJP 포트를 통해) 연동하므로, 아파치의 mod_jk.log 를 봤다. (ex : Apache2.2\logs\mod_jk.log) 확인 결과 과거 톰캣의 경로를 찾지 못한다는 에러메시지가 떨어져있었다. 설정파일 어딘가에 과거 톰캣 경로가 남아있다는 뜻이었다.

알고보니 https 프토토콜용(443 포트용) 설정 파일이 따로 있었다. Apache2.2\conf\httpd-ssl.conf 이었고, 해당 설정 파일을 httpd.conf 에서 인크루드하고 있었다. (cf : Include conf/httpd-ssl.conf)

httpd-ssl.conf 파일은 httpd.conf 파일처럼 DocumentRoot 값과 JkMount 값을 다 가지고 있었다. https 일 경우 httpd.conf 의 내용이 아니라 httpd-ssl.conf 파일의 내용을 참조하는 것이었다. DocumentRoot 값과 JkMount 값을 알맞게 고쳐서 해결했다.

[libGDX] 자바(JAVA) 게임 프레임워크 libGDX 시작하기

[libGDX] 자바(JAVA) 게임 프레임워크 libGDX 시작하기

멀티 플랫폼 게임 프레임워크는 C++ 로 코딩해야 하는 cocos2d-x 만 알고 있었는데 자바에도 존재했다.

(왜 자바에는 멀티 플랫폼 게임 프레임워크가 없는지 툴툴거리고 있었는데 노루발님께서 알려주셨다. 이 자리를 빌어 감사드림.)

libGDX 는 자바(JAVA)로 코딩 가능한 멀티 플랫폼 게임 프레임워크이다.

libGDX 시작하기

1. https://libgdx.badlogicgames.com/ 에 접속한다.

상단 메뉴의 [Download] 를 클릭하고, [Download Setup App] 버튼을 클릭한다.

 

2. 다운받은 gdx-setup.jar 파일을 적당한 위치에 저장한다. 필자는 C:\libgdx 폴더 안에 저장했다.

 

3. 이 시점에서 만약 Android SDK 가 무엇인지 모르거나 안드로이드 스튜디오(Android Studio)가 설치되어 있지 않다면 먼저 안드로이드 스튜디오를 설치한다.

안드로이드 스튜디오는 https://developer.android.com/studio/archive?hl=ko 주소에서 다운로드 받을 수 있으며 다운로드 약관에 동의하면 아래 화면이 나온다.

안드로이드 스튜디오 버전 4.0 이상을 추천한다. (ex : Android Studio 4.1)

exe 파일보다는 zip 파일로 다운받는 것을 추천하며 zip 파일 압축을 적당한 위치에 풀어서 사용하면 된다.

안드로이드 스튜디오를 실행하여 [Configure] – [SDK Manager] 메뉴를 클릭한다.

 

아래 스크린샷처럼 Android SDK Location 이 표시되어 있다. (ex : C:\Users\사용자명\AppData\Local\Android\Sdk)

이 경로를 잘 기억해두자.

 

4. 아까 다운받았던 gdx-setup.jar 파일을 실행한다. 잘 실행되지 않으면 cmd 에서 [java -jar 파일경로] 명령어로 실행하면 된다. (ex : java -jar C:\libgdx\gdx-setup.jar)

java 명령이 실행되지 않는다면 JDK를 설치해야 한다. 검색을 동원해서 JDK1.8 버전을 설치하길 바란다. (ex : jdk1.8.0_171)

실행결과 아래 창이 뜨면 Destination 항목에 C:\libgdx\test 를 입력한다.

Android SDK 항목에 실제 Android SDK 파일경로를 입력한다. (ex : C:\Users\사용자명\AppData\Local\Android\Sdk)

Sub Projects 항목의 Desktop, Android, Ios, Html 체크박스 모두에 체크한다.

이후 [Generate] 버튼을 클릭한다.

[Generate] 버튼을 클릭하고 조금 기다리면 콘솔창에 메시지가 뜬다.

프로젝트를 이클립스에서 여는 방법, 인텔리제이에서 여는 방법, 넷빈즈에서 여는 방법이 쓰여 있는데 우리는 안드로이드 스튜디오에서 열 것이다.

참고로 안드로이드 스튜디오는 인텔리제이 기반의 툴이다.

5. 안드로이드 스튜디오를 실행하고 [Open an Existing Project] 항목을 클릭한다. test 프로젝트를 선택하고(ex : C:\libgdx\test) [OK] 버튼을 클릭한다.

6. 우측상단의 [android] 를 클릭하고 [Edit Configurations…] 버튼을 클릭한다.

윈도우가 표시되면 좌측상단의 [+] 기호를 클릭하고 [Application] 을 선택한다.

 

 

7. Main Class 항목은 DesktopLauncher 를 선택한다. (ex : com.mygdx.game.desktop.DesktopLauncher)

Use classpath of module 항목은 [test.desktop] 을 선택한다.

[OK] 버튼을 클릭한다.

 

 

8. 예제 프로그램이 정상적으로 실행되면 성공이다.


참고사이트 1 : https://blog.naver.com/jogilsang/221073600345

참고사이트 2 : https://github.com/libgdx/libgdx/wiki/Project-Setup-Gradle

[Xcode] No signing certificate “iOS Distribution” found

[Xcode] No signing certificate “iOS Distribution” found

Xcode 에서 Provisioning Profile 를 선택했을 때 No signing certificate “iOS Distribution” found 오류가 발생하는 경우.

일단 애플의 Certificates 개념에 대해 알아야 한다.

애플 App은 (1) CSR 을 만들고, (2) 그 CSR 로 인증서(Certificate) 를 만들고, (3) 그 인증서로 프로비저닝 프로파일(Provisioing Profile) 를 만든다. 이렇게 만든 프로비저닝 프로파일을 App에 씌워서 Debug 도 실행하고 Relase 도 실행하는 식이다.

참고로 인증서(Certificate)는 developer.apple.com 의 좌측 메뉴 Certificates 에서 만들 수 있다.

프로비저닝 프로파일(Provisioing Profile)는 developer.apple.com 의 좌측 메뉴 Profiles 에서 만들 수 있다.

 

그런데 이 인증서(Certificate)를 아무나 사용할 수 있는 것이 아니라 개인키가 들어있어야 한다.

키체인에 들어가서 해당 인증서를 열어봤을 때(항목을 더블클릭해서 확장했을 때) 개인키가 들어있지 않은 경우, 그런 인증서에 대해 Xcode 에서는 No signing certificate “iOS Distribution” found 오류를 표시한다.

No signing certificate “iOS Distribution” found 오류를 해결하는 방법은 2가지가 있다.

1. CSR 부터 새로 만들기 (기존 인증서 무시하고 새로 만들어도 되는 경우)

해당 맥에서 (1) CSR 을 만들고, (2) 그 CSR 로 인증서(Certificate) 를 만들고, (3) 그 인증서로 프로비저닝 프로파일(Provisioing Profile) 을 새로 만들면 된다.

CSR을 만드는 법은 아래와 같다.

(1) 사용자 이메일 주소 입력

(2) 일반 이름 입력. 회사명일 경우 회사명을 정확히 입력

(3) CA 이메일 주소 : 비움

(4) [디스크에 저장됨] 라디오 버튼 체크

(5) [본인이 키 쌍 정보 지정]은 체크할 필요 없음

(6) [계속] 클릭

한 번 만들어놓은 CSR은 계속 재사용이 가능하다. 이메일과 기관명이 동일하다면(동일한 엔터프라이즈 계정 내에서 인증서 발급이 필요하다면) 한 번 만들어놓은 CSR을 계속 사용하면 된다.

2. CSR 부터 새로 만들 수 없는 경우 (기존 인증서를 사용하고 싶은 경우)

일단 키체인에 해당 인증서와 함께 개인키가 등록되어 있는 맥을 찾아야 한다.

주로 해당 PC에서 인증서를 만든게 아니라 다른 PC에서(또는 다른 맥 계정에서) 인증서를 만든 경우가 여기에 해당한다.

예를 들어 CSR을 만들었던 맥의 계정으로 접근해서 키체인을 연다.

그리고 키체인에서 해당 인증서를 찾고, 항목을 더블클릭해서 개인키가 같이 보이도록 만든다.

해당 인증서와 개인키를 함께 선택하고(Shift + 클릭) 마우스 우클릭해서 [2개 항목 내보내기…] 를 클릭한다.

파일 포맷은 [개인 정보 교환(.p12)] 확장자로 해서 저장한다.

생성된 p12 파일을 No signing certificate “iOS Distribution” found 오류가 발생했던 맥으로 옮긴다.

해당 맥에서 p12 파일 아이콘을 더블클릭해서 키체인에 인증서와 개인키를 등록하면 된다.

참고사이트 : https://qastack.kr/programming/12867878/missing-private-key-in-the-distribution-certificate-on-keychain

[Xcode] Provisioning Profiles 삭제 (프로비저닝 프로파일 삭제)

[Xcode] Provisioning Profiles 삭제 (프로비저닝 프로파일 삭제)

Xcode 의 Provisioning Profile 콤보박스 내용을 삭제하여 정리하고 싶은 경우.

아래 폴더 안의 내용을 삭제하면 된다.

~/Library/MobileDevice/Provisioning Profiles

런치패드(LunchPad) – 터미널(Terminal) 을 실행해서 아래와 같이 입력하면 파일을 조회할 수 있다.

cd ~/Library/MobileDevice/Provisioning\ Profiles

ls -lrt

참고사이트 : http://minsone.github.io/mac/ios/delete-provisioning-profiles

[JAVA] html 정규식으로 img src 어트리뷰트 치환

[JAVA] html 정규식으로 img src 어트리뷰트 치환

주어진 html 코드(String 변수)에서 src 부분만 가져와서 원하는 문자열로 치환하는 코드.

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReplaceTest {

    public static void main(String[] args) {
        StringBuffer htmlBuff = new StringBuffer();
        htmlBuff.append(“<html>\n”);
        htmlBuff.append(“<body>\n”);
        htmlBuff.append(“<img src=\”1.jpg\”/>\n”);
        htmlBuff.append(“<img src=\”2.jpg\”/>\n”);
        htmlBuff.append(“<img src=\”data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVBhXY3gro8LA8B9MQqn/MDEZFQCUAgne4kekCgAAAABJRU5ErkJggg==\”/>\n”);
        htmlBuff.append(“</body>\n”);
        htmlBuff.append(“</html>\n”);
        
        String html = htmlBuff.toString();
        System.out.println(“[AS-IS]”);
        System.out.println(html);
        
        Pattern imgSrcPattern = Pattern.compile(“<img[^>]*src=[\”‘]?([^>\”‘]+)[\”‘]?[^>]*>”);
        Matcher matcher = imgSrcPattern.matcher(html);
        while (matcher.find()) {
            String imgSrc = matcher.group(1);
            if (imgSrc == null || imgSrc.length() == 0) {
                continue;
            }
            
            // BASE64 이미지 skip
            if (imgSrc.trim().startsWith(“data:image”)) {
                continue;
            }
            
            String newUrl = http://localhost/img/ + imgSrc;
            html = html.replace(imgSrc, newUrl);
        }

        System.out.println(“[TO-BE]”);
        System.out.println(html);
    }
}

실행결과

[AS-IS]
<html>
<body>
<img src=”1.jpg”/>
<img src=”2.jpg”/>
<img src=”data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVBhXY3gro8LA8B9MQqn/MDEZFQCUAgne4kekCgAAAABJRU5ErkJggg==”/>
</body>
</html>

[TO-BE]
<html>
<body>
<img src=”http://localhost/img/1.jpg”/>
<img src=”http://localhost/img/2.jpg”/>
<img src=”data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVBhXY3gro8LA8B9MQqn/MDEZFQCUAgne4kekCgAAAABJRU5ErkJggg==”/>
</body>
</html>

단, 위의 코드에는 문제점이 있다.

replace 로 치환하기 때문에 원하는 부분이 아닌 다른 부분이 치환될 수 있다는 것.

예를 들어 img src 어트리뷰트의 값이 1.jpg 일 때 아래 두 가지 문제가 있다.

(1) 다른 img 태그의 src 값이 동일한 1.jpg 일 때 같이 치환되는 문제

(2) img 태그와 상관없이 본문에 들어있는 1.jpg 문자열 값도 치환되는 문제

이런 점이 크게 문제가 되지 않은 경우 그대로 쓰면 된다.

그렇지 않을 경우 아래처럼 개선할 필요성이 있다.

(1) 2개 이상 발견될 경우에도 첫번째로 발견된 부분만 치환해야 함.

replaceFirst 가 알맞지만 이건 정규식이 적용되는 메서드라서 위험하므로 직접 메서드를 만들어쓰는 것을 추천한다. (cf : replaceOne)

참고사이트 : https://blog.naver.com/bb_/220638989936

(2) 치환하고자 하는 부분(src 어트리뷰트 값)만 치환하여 html 본문내용 훼손이 없어야 함

[Android] Execution failed for task ‘:app:transformClassesAndResourcesWithProguardForRelease’

[Android] Execution failed for task ‘:app:transformClassesAndResourcesWithProguardForRelease’

디버그 시에는 앱이 잘 실행되는데, 앱을 APK 로 만들면

Execution failed for task ‘:app:transformClassesAndResourcesWithProguardForRelease’ 오류가 발생하는 경우.

:app:transformClassesAndResourcesWithProguardForRelease 오류를 검색해보니 아래 내용을 찾을 수 있었다.

사용하는 라이브러리마다 참조하는 라이브러리 버전이 다 다를 경우, 메소드의 수가 계속 증가하여 64k개 이상을 가질 경우 위와 같은 에러가 발생한다.

(중략)

확실한 해결 방법은 버전이 다른 라이브러리를 사용할 경우 하나로 고정시키는 방법이다.

(출처 : https://miraclehwan.tistory.com/22)​

내 경우 안드로이드 프로젝트에서 특정 라이브러리를 새 버전으로 교체했다가 위 오류가 발생했다.
정확히 표현하면 기존 라이브러리(jar 확장자 파일, 예를 들어 A1.jar)을 삭제하고, 특정 라이브러리(aar 확장자 파일, 예를 들어 A2.aar)를 추가한 결과 위 오류가 발생했다.

내가 이해한 바는, 결국 라이브러리 충돌이 난다는 얘기인 것이다.
분명히 기존 라이브러리(A1.jar)를 지웠는데 말이다.

로그캣과 디버그 콘솔을 꼼꼼히 읽어보니 못보던 패키지명이 있었다.
패키지명을 쫓아가보니 내가 모르는 jar에 속했다. 다시 말해 어떤 jar을 하나 더 의존하는 것처럼 보였다. 이 jar를 B.jar 라고 하자.

A1.jar 와 함께 B.jar 까지 삭제했더니 위 오류가 해결됐다.

아래 테스트를 통해 더 확실히 알 수 있었다.
기존 앱에서 A1.jar 파일은 그대로 두고, B.jar 를 삭제했다.
앱에 아무런 빨간줄도 나오지 않았지만, 실행해보면 앱은 오류가 발생됐다.

앱은 직접적으로 A1.jar 만 의존하고 있지만, A1.jar 가 B.jar 를 의존하고 있어서 둘 다 지워야하는 대상이었던 것 같다.

[Android] Caused by: java.lang.VerifyError: Verifier rejected class

[Android] Caused by: java.lang.VerifyError: Verifier rejected class



 

안드로이드에서 아래와 같이 java.lang.VerifyError 발생한 경우.

Caused by: java.lang.VerifyError: Verifier rejected class com.packageName.className: void
     com.packageName.className<init>(boolean) failed to verify: void
     com.packageName.className<init>(boolean): [0x31] Expected initialization on uninitialized reference Precise Reference: java.lang.String (declaration of ‘com.packageName.className’ appears in /data/app/com.packageName-1/split_lib_slice_9_apk.apk)

java.lang.VerifyError 오류를 검색해보니 아래 내용을 찾을 수 있었다.

1. 컴파일 시 사용한 라이브러리와 런타임 시 사용한 라이브러리 버전이 달라서 메서드 형태가 다른 경우

2. 사용한 라이브러리가 상위 버전의 JDK에서 컴파일 된 경우

=> 결국은 라이브러리 버전을 맞춰야 한다.

(출처 : https://hiro1983.tistory.com/m/29)

내 경우 안드로이드 프로젝트에 특정 라이브러리(aar 확장자 파일)를 추가하는 과정에서 java.lang.VerifyError 오류가 발생했다.

일단 안드로이드 프로젝트와 aar 파일과의 JDK버전 내지는 안드로이드 SDK 버전을 맞춰야 한다는 생각이 들었다.
해당 aar 을 반디집으로 풀어서 AndroidManifest.xml 을 열어보니 안드로이드 버전 29로 되어있었다.
내 프로젝트는 안드로이드 버전 28 이었는데, 어쩐 일인지 build.gradle을 29로 고치면 앱이 동작하지 않았다.


검색해보니 안드로이드 스튜디오 버전 4.0 이상에서 안드로이드 버전 29를 쓸 수 있다는 내용을 보았다.
내가 사용한 안드로이드 스튜디오는 버전 3.4 였다.

결국 안드로이드 스튜디오 4.0을 새로 설치하고, 작업중인 프로젝트를 로드했다.


결과적으로 안드로이드 스튜디오 4.0 에서는 VerifyError 가 발생하지 않았다.
버전을 29로 올리지도 않았다.
그냥 내 프로젝트는 여전히 버전 28이고, aar 은 버전 29였다.


추측이지만 해결된 이유는 aar 모듈이 안드로이드 스튜디오 4에서 만들어진 모듈이라서, 그걸 사용하는 프로젝트 역시 안드로이드 스튜디오 4에서 빌드해야 되는 것 같다.​

[Android] 안드로이드 웹뷰(WebView) 파일 다운로드

[Android] 안드로이드 웹뷰(WebView) 파일 다운로드

안드로이드 하이브리드 앱의 웹뷰(WebView) 안에서 파일 다운로드 URL을 호출했을 때 아무 반응이 없는 경우.

오류가 아니고 원래 안드로이드는 웹뷰에 DownloadListener를 구현해줘야만 파일 다운로드 가능하다.

해당 웹뷰를 사용하는 Activity(정확히 표현하면 Activity 클래스를 상속받은 특정 Activity) 의 onCreate 메서드 안에 다음과 같이 작성한다.

public class OOOActivity extends Activity {

    public Activity thisActivity = null;

    public WebView webView = null;

 

    (중략)

  

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        thisActivity = this;

        (중략)

        // 안드로이드 파일 다운로드 구현 (웹메일 첨부 다운로드)
        webView.setDownloadListener(new DownloadListener() {
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
                try {
                    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
                    DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);

                    contentDisposition = URLDecoder.decode(contentDisposition,”UTF-8″);

                    // 파일명 잘라내기

                    String fileName = contentDisposition;
                    if (fileName != null && fileName.length() > 0) {
                        int idxFileName = fileName.indexOf(“filename=”);
                        if (idxFileName > -1) {
                            fileName = fileName.substring(idxFileName + 9).trim();
                        }

                        if (fileName.endsWith(“;”)) {
                            fileName = fileName.substring(0, fileName.length() – 1);
                        }

                        if (fileName.startsWith(“\””) && fileName.startsWith(“\””)) {
                            fileName = fileName.substring(1, fileName.length() – 1);
                        }
                    }

                    // 세션 유지를 위해 쿠키 세팅하기
                    String cookie = CookieManager.getInstance().getCookie(url);
                    request.addRequestHeader(“Cookie”, cookie);

                    request.setMimeType(mimetype);
                    request.addRequestHeader(“User-Agent”, userAgent);
                    request.setDescription(“Downloading File”);
                    request.setAllowedOverMetered(true);
                    request.setAllowedOverRoaming(true);
                    request.setTitle(fileName);
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        request.setRequiresCharging(false);
                    }

                    request.allowScanningByMediaScanner();
                    request.setAllowedOverMetered(true);
                    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
                    request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);

                    dm.enqueue(request);
                    Toast.makeText(getApplicationContext(),”파일을 다운로드 합니다.”, Toast.LENGTH_LONG).show();
                }
                catch (Exception e) {
                    if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                        if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                            Toast.makeText(getBaseContext(), “파일 다운로드 권한을 허용해주십시오.”, Toast.LENGTH_LONG).show();
                            ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1004);
                        }
                        else {
                            Toast.makeText(getBaseContext(), “파일 다운로드 권한을 허용해주십시오.”, Toast.LENGTH_LONG).show();
                            ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1004);
                        }
                    }
                }
            }
        });

(후략)

이어서 AndroidManifest.xml 에 다음과 같이 권한 코드를 넣는다.

    <!– 파일 다운로드 권한 –>
    <uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” />
    <uses-permission android:name=”android.permission.READ_EXTERNAL_STORAGE” />

파일 다운로드가 수행되지 않는 경우가 발견되어 내용을 추가한다. URL이 파일명으로 끝나지 않는 경우에만 그랬다. 처음엔 https 문제인 것으로 판단했는데, 드랍박스에 파일을 업로드해서 테스트 해본 결과 https 도 잘 동작했다.

확인해보니 파일 다운로드 앞단에서 세션을 체크하는 경우였다. 파일 다운로드가 정상적으로 수행되는 것처럼 보이지만 용량이 3.28kb 로 나오는 등 턱없이 작았다.

아래 코드를 추가하자 해결됐다.

// 세션 유지를 위해 쿠키 세팅하기
String cookie = CookieManager.getInstance().getCookie(url);
request.addRequestHeader(“Cookie”, cookie);

참고사이트 1 : https://onedaycodeing.tistory.com/71

참고사이트 2 : https://stackoverrun.com/ko/q/4885896 

[Javascript] charCodeAt / String.fromCharCode

[Javascript] charCodeAt / String.fromCharCode

자바스크립트 charCodeAt 반대는 String.fromCharCode

자바스크립트 String.fromCharCode 반대는 charCodeAt

1. charCodeAt 예제

var str = “A”;
var charCode = str.charCodeAt(0);
// charCode == 65

2. String.fromCharCode 예제

var charCode = 65;
var str = String.fromCharCode(charCode);
// str == “A”

참고사이트 : https://www.phpschool.com/gnuboard4/bbs/board.php?bo_table=qna_html&wr_id=15917